diff --git a/.github/workflows/avoid-typos.yml b/.github/workflows/avoid-typos.yml index 68a17b5ba32..ce792cbde4c 100644 --- a/.github/workflows/avoid-typos.yml +++ b/.github/workflows/avoid-typos.yml @@ -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 diff --git a/.github/workflows/docs-tests.yml b/.github/workflows/docs-tests.yml new file mode 100644 index 00000000000..dbfaa723da0 --- /dev/null +++ b/.github/workflows/docs-tests.yml @@ -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 diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 47563fba1ef..a28d54e3f7c 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -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 diff --git a/.github/workflows/studio-tests.yml b/.github/workflows/studio-tests.yml index 4a540f652a8..f0a18fcfb88 100644 --- a/.github/workflows/studio-tests.yml +++ b/.github/workflows/studio-tests.yml @@ -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: ./ diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 0898d33a8c1..46ad011a07c 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index c78e6a2eb47..0b50b45e703 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.misspell-fixer.ignore b/.misspell-fixer.ignore index 7c90f6e4788..105b2fccda8 100644 --- a/.misspell-fixer.ignore +++ b/.misspell-fixer.ignore @@ -1 +1 @@ -^./i18n +^./i18n \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..4fd021952d5 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000000..25bf17fc5aa --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 \ No newline at end of file diff --git a/DEVELOPERS.md b/DEVELOPERS.md index e88f3cb89f0..1a4e1a1e27c 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -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//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 diff --git a/README.md b/README.md index bf770cbaa39..b71944fad9c 100644 --- a/README.md +++ b/README.md @@ -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 Client Feature-Clients (bundled in Supabase client) + Supabase @@ -99,7 +101,9 @@ Our approach for client libraries is modular. Each sub-library is a standalone i storage-lang END ROW --> + ⚡️ Official ⚡️ + JavaScript (TypeScript) supabase-js @@ -118,7 +122,9 @@ Our approach for client libraries is modular. Each sub-library is a standalone i storage-dart functions-dart + 💚 Community 💚 + C# supabase-csharp @@ -200,6 +206,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i storage-gdscript functions-gdscript + @@ -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) ---- - -## Sponsors - -[![New Sponsor](https://user-images.githubusercontent.com/10214025/90518111-e74bbb00-e198-11ea-8f88-c9e3c1aa4b5b.png)](https://github.com/sponsors/supabase) diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore index ba0b52697ed..b18b47afe4b 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -14,6 +14,7 @@ .env.test.local .env.staging.local .env.production.local +*.swp npm-debug.log* yarn-debug.log* diff --git a/apps/docs/codeHikeTheme.js b/apps/docs/codeHikeTheme.js deleted file mode 100644 index e34b5b89779..00000000000 --- a/apps/docs/codeHikeTheme.js +++ /dev/null @@ -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', - }, - }, - ], -} diff --git a/apps/docs/components/Admonition.tsx b/apps/docs/components/Admonition.tsx index 42f7049f864..8a85475d929 100644 --- a/apps/docs/components/Admonition.tsx +++ b/apps/docs/components/Admonition.tsx @@ -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 = ({ type = 'note', label, children }) => { +const Admonition = ({ type = 'note', label, children }: PropsWithChildren) => { return (
=> { + 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 ( + <> + setTeamID(e.target.value.trim())} + /> + setServiceID(e.target.value.trim())} + /> + setKeyID(e.target.value.trim())} + /> +
+ { + setFile({ file: e.target.files[0] }) + }} + /> +
+
+ + + + {error && {error}} + + {secretKey && ( + <> +
+ + + )} + + ) +} + +export default AppleSecretGenerator diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.test.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.test.ts new file mode 100644 index 00000000000..2d2d1285229 --- /dev/null +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.test.ts @@ -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 ') + }) + }) + }) +}) diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts index 0103cef6aa7..844724a5466 100644 --- a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts @@ -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 diff --git a/apps/docs/components/FooterHelpCallout.tsx b/apps/docs/components/FooterHelpCallout.tsx index 89f3265ec16..429ed15d4e6 100644 --- a/apps/docs/components/FooterHelpCallout.tsx +++ b/apps/docs/components/FooterHelpCallout.tsx @@ -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?', diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx index edf3252ca64..3bf27c6b2a0 100644 --- a/apps/docs/components/HomePageCover.tsx +++ b/apps/docs/components/HomePageCover.tsx @@ -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 = () => (
-
+
@@ -29,77 +72,19 @@ const HomePageCover = (props) => { Discover how to set up a database to an app making queries in just a few minutes.

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ {frameworks.map((framework, i) => ( + + + + + + ))}
@@ -118,7 +103,7 @@ const HomePageCover = (props) => {

-
+
diff --git a/apps/docs/components/MDX/ai/quickstart_hf_deployment.mdx b/apps/docs/components/MDX/ai/quickstart_hf_deployment.mdx new file mode 100644 index 00000000000..595b1f8f25b --- /dev/null +++ b/apps/docs/components/MDX/ai/quickstart_hf_deployment.mdx @@ -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. diff --git a/apps/docs/components/MDX/database_setup.mdx b/apps/docs/components/MDX/database_setup.mdx new file mode 100644 index 00000000000..2a6e853f9c2 --- /dev/null +++ b/apps/docs/components/MDX/database_setup.mdx @@ -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. diff --git a/apps/docs/components/MDX/storage_management.mdx b/apps/docs/components/MDX/storage_management.mdx index 1eebf7cb575..e8f0184b88e 100644 --- a/apps/docs/components/MDX/storage_management.mdx +++ b/apps/docs/components/MDX/storage_management.mdx @@ -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' diff --git a/apps/docs/components/Navigation/Footer.tsx b/apps/docs/components/Navigation/Footer.tsx index e14892a8f39..600f81f40e8 100644 --- a/apps/docs/components/Navigation/Footer.tsx +++ b/apps/docs/components/Navigation/Footer.tsx @@ -55,105 +55,87 @@ const Footer = () => ( ))}
- - - - + + +
diff --git a/apps/docs/components/Navigation/Navigation.types.ts b/apps/docs/components/Navigation/Navigation.types.ts index a644cc7c70d..3882fb26e77 100644 --- a/apps/docs/components/Navigation/Navigation.types.ts +++ b/apps/docs/components/Navigation/Navigation.types.ts @@ -9,8 +9,8 @@ export interface NavMenuGroup { export interface NavMenuSection { name: string - url?: string - items: NavMenuSection[] + url?: `/${string}` + items: Partial[] } 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> +}> diff --git a/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx index fb51bb98ba7..0cf04315a69 100644 --- a/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx @@ -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' diff --git a/apps/docs/components/Navigation/NavigationMenu/HomeMenuIconPicker.tsx b/apps/docs/components/Navigation/NavigationMenu/HomeMenuIconPicker.tsx index a2b9fa52511..bc7c67c60d3 100644 --- a/apps/docs/components/Navigation/NavigationMenu/HomeMenuIconPicker.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/HomeMenuIconPicker.tsx @@ -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 case 'storage': return + case 'ai': + return case 'platform': return case 'resources': diff --git a/apps/docs/components/Navigation/NavigationMenu/HomeMenuIcons.tsx b/apps/docs/components/Navigation/NavigationMenu/HomeMenuIcons.tsx index 415c29f1b07..6925319d68d 100644 --- a/apps/docs/components/Navigation/NavigationMenu/HomeMenuIcons.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/HomeMenuIcons.tsx @@ -32,7 +32,7 @@ export function IconMenuApi({ width = 16, height = 16 }: HomeMenuIcon) { > @@ -363,6 +363,26 @@ export function IconMenuStorage({ width = 16, height = 16 }: HomeMenuIcon) { ) } +export function IconMenuAI({ width = 16, height = 16 }: HomeMenuIcon) { + return ( + + + + ) +} + export function IconMenuSwift({ width = 16, height = 16 }: HomeMenuIcon) { return ( diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index bf1353c87fe..aa1116135d7 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -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' }, ], diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx index 2c5ad1a6734..452eb693116 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx @@ -68,6 +68,11 @@ const menus: Menu[] = [ path: '/guides/storage', type: 'guide', }, + { + id: 'ai', + path: '/guides/ai', + type: 'guide', + }, { id: 'platform', path: '/guides/platform', diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index 2f683b429f7..f264a4112b7 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -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) } }) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx index 0069b311827..a280bf07da6 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx @@ -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 = ({
-
    +
      {filteredSections.map((section) => { return ( diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx index 15dd0f831d1..c96cac5fd92 100644 --- a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx @@ -125,16 +125,15 @@ const TopNavBar: FC = () => {
-
  • diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx index 826dda8003d..bd080d8e7c9 100644 --- a/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx @@ -105,56 +105,40 @@ const TopNavBarRef: FC = () => {
- + + - Supabase.com - - - -
    -
  • -
    toggleTheme()}> - {isDarkMode ? ( - - ) : ( - - )} -
    -
  • -
+ + + + +
toggleTheme()}> + {isDarkMode ? ( + + ) : ( + + )} +
diff --git a/apps/docs/components/StepHikeCompact/index.tsx b/apps/docs/components/StepHikeCompact/index.tsx index 6ef75c7140c..1daa76568d1 100644 --- a/apps/docs/components/StepHikeCompact/index.tsx +++ b/apps/docs/components/StepHikeCompact/index.tsx @@ -29,7 +29,7 @@ const StepHikeCompact: FC & IStepHikeCompactSubcomponents = ({ const Step: FC = ({ children, title, step }) => { return ( -
+
{shortText}} - {item.description && (
{item.description}
)} - {item.notes && (
- {item.notes} + + {item.notes} +
)} {/* // parameters */} diff --git a/apps/docs/components/reference/RefSectionHandler.tsx b/apps/docs/components/reference/RefSectionHandler.tsx index df5ed1c5700..26b3f0350a4 100644 --- a/apps/docs/components/reference/RefSectionHandler.tsx +++ b/apps/docs/components/reference/RefSectionHandler.tsx @@ -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 ( <> - {pageTitle} - + {fullTitle} + + {props.isOldVersion && } diff --git a/apps/docs/data/authProviders.ts b/apps/docs/data/authProviders.ts index ea5855061bd..a1053a30772 100644 --- a/apps/docs/data/authProviders.ts +++ b/apps/docs/data/authProviders.ts @@ -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, diff --git a/apps/docs/docs/ref/csharp/release-notes.mdx b/apps/docs/docs/ref/csharp/release-notes.mdx index 777621307dd..291636cfb08 100644 --- a/apps/docs/docs/ref/csharp/release-notes.mdx +++ b/apps/docs/docs/ref/csharp/release-notes.mdx @@ -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` (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 diff --git a/apps/docs/docs/ref/javascript/v1/upgrade-guide.mdx b/apps/docs/docs/ref/javascript/v1/upgrade-guide.mdx index d13e0311cae..d031f4a0778 100644 --- a/apps/docs/docs/ref/javascript/v1/upgrade-guide.mdx +++ b/apps/docs/docs/ref/javascript/v1/upgrade-guide.mdx @@ -41,7 +41,7 @@ _Optionally_ if you are using custom configuration with `createClient` then foll > -```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, { -```ts title=src/supabaseClient.ts +```ts src/supabaseClient.ts const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { db: { schema: 'custom', diff --git a/apps/docs/docs/reference/dart/initializing.mdx b/apps/docs/docs/reference/dart/initializing.mdx index ba76c217cce..80c69925a6a 100644 --- a/apps/docs/docs/reference/dart/initializing.mdx +++ b/apps/docs/docs/reference/dart/initializing.mdx @@ -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 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 main() async { await Supabase.initialize( url: 'https://xyzcompany.supabase.co', diff --git a/apps/docs/jest.config.ts b/apps/docs/jest.config.ts new file mode 100644 index 00000000000..348b75c0bb7 --- /dev/null +++ b/apps/docs/jest.config.ts @@ -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 diff --git a/apps/docs/layouts/SiteLayout.tsx b/apps/docs/layouts/SiteLayout.tsx index ff35c1af441..adc866288b0 100644 --- a/apps/docs/layouts/SiteLayout.tsx +++ b/apps/docs/layouts/SiteLayout.tsx @@ -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', diff --git a/apps/docs/layouts/guides/index.tsx b/apps/docs/layouts/guides/index.tsx index ee025a2b358..b637c118b20 100644 --- a/apps/docs/layouts/guides/index.tsx +++ b/apps/docs/layouts/guides/index.tsx @@ -139,7 +139,7 @@ const Layout: FC = (props) => {
Edit this page on GitHub diff --git a/apps/docs/lib/mdx/generateRefMarkdown.tsx b/apps/docs/lib/mdx/generateRefMarkdown.tsx index 5d959560ade..6e71c0c2660 100644 --- a/apps/docs/lib/mdx/generateRefMarkdown.tsx +++ b/apps/docs/lib/mdx/generateRefMarkdown.tsx @@ -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 }) diff --git a/apps/docs/lib/mdx/plugins/rehypeLinkTransform.ts b/apps/docs/lib/mdx/plugins/rehypeLinkTransform.ts new file mode 100644 index 00000000000..5eab44f885d --- /dev/null +++ b/apps/docs/lib/mdx/plugins/rehypeLinkTransform.ts @@ -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) + }) + } +} diff --git a/apps/docs/lib/mdx/plugins/remarkAdmonition.ts b/apps/docs/lib/mdx/plugins/remarkAdmonition.ts new file mode 100644 index 00000000000..343c58ad419 --- /dev/null +++ b/apps/docs/lib/mdx/plugins/remarkAdmonition.ts @@ -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 diff --git a/apps/docs/lib/mdx/plugins/remarkRemoveTitle.ts b/apps/docs/lib/mdx/plugins/remarkRemoveTitle.ts new file mode 100644 index 00000000000..3dcce035b60 --- /dev/null +++ b/apps/docs/lib/mdx/plugins/remarkRemoveTitle.ts @@ -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) + } + } + } +} diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index d33090df77e..682af108190 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -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, diff --git a/apps/docs/package.json b/apps/docs/package.json index 28ce0d084d8..5d79d59dddd 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -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" } } diff --git a/apps/docs/pages/_app.tsx b/apps/docs/pages/_app.tsx index 6b24c000d4b..1ddc56fd1fa 100644 --- a/apps/docs/pages/_app.tsx +++ b/apps/docs/pages/_app.tsx @@ -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 diff --git a/apps/docs/pages/guides/ai.mdx b/apps/docs/pages/guides/ai.mdx new file mode 100644 index 00000000000..2153bdde029 --- /dev/null +++ b/apps/docs/pages/guides/ai.mdx @@ -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. + +
+ {examples.map((x) => ( + + ))} +
+ +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 + +
+ {integrations.map((x) => ( + + ))} +
+ +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 + +
+ {customers.map((x) => ( + + ))} +
+ +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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/choosing-instance-type.mdx b/apps/docs/pages/guides/ai/choosing-instance-type.mdx new file mode 100644 index 00000000000..c6bb503c4aa --- /dev/null +++ b/apps/docs/pages/guides/ai/choosing-instance-type.mdx @@ -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` + + + +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. + + + +## 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. + +
+ multi database + multi database +
+ +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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/concepts.mdx b/apps/docs/pages/guides/ai/concepts.mdx new file mode 100644 index 00000000000..c35a6e72aed --- /dev/null +++ b/apps/docs/pages/guides/ai/concepts.mdx @@ -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: + +Vector similarity + +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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/engineering-for-scale.mdx b/apps/docs/pages/guides/ai/engineering-for-scale.mdx new file mode 100644 index 00000000000..663550fc4a8 --- /dev/null +++ b/apps/docs/pages/guides/ai/engineering-for-scale.mdx @@ -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): + +
+ single database + single database +
+ +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. + +
+ With secondaries + With secondaries +
+ +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: + +
+ multi database + multi database +
+ +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/examples/building-chatgpt-plugins.mdx b/apps/docs/pages/guides/ai/examples/building-chatgpt-plugins.mdx new file mode 100644 index 00000000000..1f34a54c413 --- /dev/null +++ b/apps/docs/pages/guides/ai/examples/building-chatgpt-plugins.mdx @@ -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: + +diagram reference + +diagram reference + +### 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= +export DATASTORE=supabase +export SUPABASE_URL= +export SUPABASE_SERVICE_ROLE_KEY= +``` + +For Postgres datastore, you'll need to export these environment variables instead: + +```bash +export OPENAI_API_KEY= +export DATASTORE=postgres +export PG_HOST= +export PG_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\\" +``` + +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 }) => +export default Page diff --git a/apps/docs/pages/guides/ai/examples/headless-vector-search.mdx b/apps/docs/pages/guides/ai/examples/headless-vector-search.mdx new file mode 100644 index 00000000000..467d3788a38 --- /dev/null +++ b/apps/docs/pages/guides/ai/examples/headless-vector-search.mdx @@ -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 }) => +export default Page diff --git a/apps/docs/pages/guides/ai/examples/huggingface-image-captioning.mdx b/apps/docs/pages/guides/ai/examples/huggingface-image-captioning.mdx new file mode 100644 index 00000000000..1f891655d85 --- /dev/null +++ b/apps/docs/pages/guides/ai/examples/huggingface-image-captioning.mdx @@ -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( + // 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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/examples/image-search-openai-clip.mdx b/apps/docs/pages/guides/ai/examples/image-search-openai-clip.mdx new file mode 100644 index 00000000000..0ca6fcd19d2 --- /dev/null +++ b/apps/docs/pages/guides/ai/examples/image-search-openai-clip.mdx @@ -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 }) => + +export default Page diff --git a/apps/docs/pages/guides/getting-started/openai/vector-search.mdx b/apps/docs/pages/guides/ai/examples/nextjs-vector-search.mdx similarity index 94% rename from apps/docs/pages/guides/getting-started/openai/vector-search.mdx rename to apps/docs/pages/guides/ai/examples/nextjs-vector-search.mdx index 580d52262e0..b340a6607b6 100644 --- a/apps/docs/pages/guides/getting-started/openai/vector-search.mdx +++ b/apps/docs/pages/guides/ai/examples/nextjs-vector-search.mdx @@ -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. - - You can find the [full example on - GitHub](https://github.com/supabase-community/nextjs-openai-doc-search). - +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 ` - ```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? >
-export const Page = ({ children }) => +export const Page = ({ children }) => export default Page diff --git a/apps/docs/pages/guides/functions/examples/openai.mdx b/apps/docs/pages/guides/ai/examples/openai.mdx similarity index 50% rename from apps/docs/pages/guides/functions/examples/openai.mdx rename to apps/docs/pages/guides/ai/examples/openai.mdx index 236ad59354f..7094a8fc936 100644 --- a/apps/docs/pages/guides/functions/examples/openai.mdx +++ b/apps/docs/pages/guides/ai/examples/openai.mdx @@ -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', } -
- -
+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 diff --git a/apps/docs/pages/guides/ai/google-colab.mdx b/apps/docs/pages/guides/ai/google-colab.mdx new file mode 100644 index 00000000000..c3e02029842 --- /dev/null +++ b/apps/docs/pages/guides/ai/google-colab.mdx @@ -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', +} + + + + + +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:@: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:@: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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/hugging-face.mdx b/apps/docs/pages/guides/ai/hugging-face.mdx new file mode 100644 index 00000000000..c198e7b2349 --- /dev/null +++ b/apps/docs/pages/guides/ai/hugging-face.mdx @@ -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. + + + +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. + + + +## 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. + + + +You will need the `supabase` CLI [installed](/docs/guides/cli) for the following commands to work. + + + +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= +``` + +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`. + + + +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. + + + +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`: + +Llama wearing sunglasses example + +## 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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/integrations/llamaindex.mdx b/apps/docs/pages/guides/ai/integrations/llamaindex.mdx new file mode 100644 index 00000000000..59db32b9cd3 --- /dev/null +++ b/apps/docs/pages/guides/ai/integrations/llamaindex.mdx @@ -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). + + + +## Launching a notebook + +Launch our [LlamaIndex](https://github.com/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb) notebook in Colab: + + + + + +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://:@:/" + +# 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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/langchain.mdx b/apps/docs/pages/guides/ai/langchain.mdx new file mode 100644 index 00000000000..3f66da36b3e --- /dev/null +++ b/apps/docs/pages/guides/ai/langchain.mdx @@ -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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/managing-collections.mdx b/apps/docs/pages/guides/ai/managing-collections.mdx new file mode 100644 index 00000000000..5849f6aa526 --- /dev/null +++ b/apps/docs/pages/guides/ai/managing-collections.mdx @@ -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://:@:/" + +# 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()`. + + + +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. + + + +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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/managing-indexes.mdx b/apps/docs/pages/guides/ai/managing-indexes.mdx new file mode 100644 index 00000000000..6269919badf --- /dev/null +++ b/apps/docs/pages/guides/ai/managing-indexes.mdx @@ -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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/python/[slug].tsx b/apps/docs/pages/guides/ai/python/[slug].tsx new file mode 100644 index 00000000000..fcbd53160c0 --- /dev/null +++ b/apps/docs/pages/guides/ai/python/[slug].tsx @@ -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 ( + + + + ) +} + +/** + * Fetch markdown from external repo and transform links + */ +export const getStaticProps: GetStaticProps = 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, + } +} diff --git a/apps/docs/pages/guides/ai/quickstarts/face-similarity.mdx b/apps/docs/pages/guides/ai/quickstarts/face-similarity.mdx new file mode 100644 index 00000000000..9bc0be7c9c2 --- /dev/null +++ b/apps/docs/pages/guides/ai/quickstarts/face-similarity.mdx @@ -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. + + + +## Launching a notebook + +Launch our [`semantic_text_deduplication`](https://github.com/supabase/supabase/blob/master/examples/ai/face_similarity.ipynb) notebook in Colab: + + + + + +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://:@:/" + +# 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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/quickstarts/hello-world.mdx b/apps/docs/pages/guides/ai/quickstarts/hello-world.mdx new file mode 100644 index 00000000000..7b678dc1d20 --- /dev/null +++ b/apps/docs/pages/guides/ai/quickstarts/hello-world.mdx @@ -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 + + + +## Launching a notebook + +Launch our [`vector_hello_world`](https://github.com/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb) notebook in Colab: + + + + + +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://:@:/" + +# 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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/quickstarts/text-deduplication.mdx b/apps/docs/pages/guides/ai/quickstarts/text-deduplication.mdx new file mode 100644 index 00000000000..8d20cf9cca4 --- /dev/null +++ b/apps/docs/pages/guides/ai/quickstarts/text-deduplication.mdx @@ -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. + + + +## Launching a notebook + +Launch our [`semantic_text_deduplication`](https://github.com/supabase/supabase/blob/master/examples/ai/semantic_text_deduplication.ipynb) notebook in Colab: + + + + + +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://:@:/" + +# 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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/structured-unstructured.mdx b/apps/docs/pages/guides/ai/structured-unstructured.mdx new file mode 100644 index 00000000000..0c392c20e09 --- /dev/null +++ b/apps/docs/pages/guides/ai/structured-unstructured.mdx @@ -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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/vecs-python-client.mdx b/apps/docs/pages/guides/ai/vecs-python-client.mdx new file mode 100644 index 00000000000..0be65c7c744 --- /dev/null +++ b/apps/docs/pages/guides/ai/vecs-python-client.mdx @@ -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 }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/vector-columns.mdx b/apps/docs/pages/guides/ai/vector-columns.mdx new file mode 100644 index 00000000000..d941c164302 --- /dev/null +++ b/apps/docs/pages/guides/ai/vector-columns.mdx @@ -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 + + + + +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. + + + + +```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`. + + + + +### 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 }) => + +export default Page diff --git a/apps/docs/pages/guides/api.mdx b/apps/docs/pages/guides/api.mdx index 390dcadd33d..7ca9052585d 100644 --- a/apps/docs/pages/guides/api.mdx +++ b/apps/docs/pages/guides/api.mdx @@ -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). + + + export const Page = ({ children }) => export default Page diff --git a/apps/docs/pages/guides/api/rest/client-libs.mdx b/apps/docs/pages/guides/api/rest/client-libs.mdx index 5594ef78686..aa967a4b9eb 100644 --- a/apps/docs/pages/guides/api/rest/client-libs.mdx +++ b/apps/docs/pages/guides/api/rest/client-libs.mdx @@ -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 }) => diff --git a/apps/docs/pages/guides/auth/auth-captcha.mdx b/apps/docs/pages/guides/auth/auth-captcha.mdx index b2546a5c7b6..59f0d901318 100644 --- a/apps/docs/pages/guides/auth/auth-captcha.mdx +++ b/apps/docs/pages/guides/auth/auth-captcha.mdx @@ -90,7 +90,7 @@ const [captchaToken, setCaptchaToken] = useState() Now lets add the HCaptcha component to the JSX section of our code -```html +```jsx ``` diff --git a/apps/docs/pages/guides/auth/auth-email.mdx b/apps/docs/pages/guides/auth/auth-email.mdx index 0f833876253..ce29b1e970b 100644 --- a/apps/docs/pages/guides/auth/auth-email.mdx +++ b/apps/docs/pages/guides/auth/auth-email.mdx @@ -95,7 +95,7 @@ Future 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) diff --git a/apps/docs/pages/guides/auth/auth-helpers.mdx b/apps/docs/pages/guides/auth/auth-helpers.mdx index b9573fd1609..26b798b820b 100644 --- a/apps/docs/pages/guides/auth/auth-helpers.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers.mdx @@ -19,6 +19,14 @@ A collection of framework-specific Auth utilities for working with Supabase. description={'A pre-built React component for authenticating users.'} />
+ {/* Flutter Auth UI */} +
+ +
{/* Next.js */}
( 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 + +``` + ### 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('', ' ( ( }, }, }} - //highlight-end /> ) ``` diff --git a/apps/docs/pages/guides/auth/auth-helpers/flutter-auth-ui.mdx b/apps/docs/pages/guides/auth/auth-helpers/flutter-auth-ui.mdx new file mode 100644 index 00000000000..13af6fb5ddd --- /dev/null +++ b/apps/docs/pages/guides/auth/auth-helpers/flutter-auth-ui.mdx @@ -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. + + + +## 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 }) => + +export default Page diff --git a/apps/docs/pages/guides/auth/auth-helpers/nextjs-pages.mdx b/apps/docs/pages/guides/auth/auth-helpers/nextjs-pages.mdx new file mode 100644 index 00000000000..11d7f549a42 --- /dev/null +++ b/apps/docs/pages/guides/auth/auth-helpers/nextjs-pages.mdx @@ -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 + + + + +```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 +``` + + + + +```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 +``` + + + + +## 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 + + + + +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 ( + + + + ) +} +``` + + + + +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 ( + + + + ) +} +``` + + + + +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. + + + + +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 +``` + + + + + +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 +``` + + + + +## 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() +``` + +Retrieving a supabase client object from the SessionContext: + +```tsx +import { useSupabaseClient } from '@supabase/auth-helpers-react' +import { Database } from '../database.types' + +const supabaseClient = useSupabaseClient() +``` + +### 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({ + 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 ( + + ) + + return ( + <> + +

user:

+
{JSON.stringify(user, null, 2)}
+

client-side data fetching with RLS

+
{JSON.stringify(data, null, 2)}
+ + ) +} + +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
Hello {user.name}
+} + +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: + + + + +```jsx +import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' + +export default function ProtectedPage({ user, data }) { + return ( + <> +
Protected content for {user.email}
+
{JSON.stringify(data, null, 2)}
+
{JSON.stringify(user, null, 2)}
+ + ) +} + +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 ?? [], + }, + } +} +``` + +
+ + +```tsx +import { User, createPagesServerClient } from '@supabase/auth-helpers-nextjs' +import { GetServerSidePropsContext } from 'next' + +export default function ProtectedPage({ user, data }: { user: User; data: any }) { + return ( + <> +
Protected content for {user.email}
+
{JSON.stringify(data, null, 2)}
+
{JSON.stringify(user, null, 2)}
+ + ) +} + +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 ?? [], + }, + } +} +``` + +
+
+ +## 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. + + + + +```jsx +import { createPagesServerClient } from '@supabase/auth-helpers-nextjs' + +export default function ProtectedPage({ user, allRepos }) { + return ( + <> +
Protected content for {user.email}
+

Data fetched with provider token:

+
{JSON.stringify(allRepos, null, 2)}
+

user:

+
{JSON.stringify(user, null, 2)}
+ + ) +} + +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 } } +} +``` + +
+ + +```tsx +import { User, createPagesServerClient } from '@supabase/auth-helpers-nextjs' +import { GetServerSidePropsContext } from 'next' + +export default function ProtectedPage({ user, allRepos }: { user: User; allRepos: any }) { + return ( + <> +
Protected content for {user.email}
+

Data fetched with provider token:

+
{JSON.stringify(allRepos, null, 2)}
+

user:

+
{JSON.stringify(user, null, 2)}
+ + ) +} + +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 } } +} +``` + +
+
+ +## Protecting API routes + +Create a server supabase client to retrieve the logged in user's session: + + + + +```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 +``` + + + + +```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 +``` + + + + +## 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`: + + + + +```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) +}) +``` + + + + +```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 +``` + + + + +#### `withPageAuth` deprecated! + +Use `createPagesServerClient` within `getServerSideProps`: + + + + +```tsx pages/profile.tsx +import { withPageAuth, User } from '@supabase/auth-helpers-nextjs' + +export default function Profile({ user }: { user: User }) { + return
{JSON.stringify(user, null, 2)}
+} + +export const getServerSideProps = withPageAuth({ redirectTo: '/' }) +``` + +
+ + +```tsx pages/profile.js +import { createPagesServerClient, User } from '@supabase/auth-helpers-nextjs' +import { GetServerSidePropsContext } from 'next' + +export default function Profile({ user }: { user: User }) { + return
{JSON.stringify(user, null, 2)}
+} + +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, + }, + } +} +``` + +
+
+ +#### `withMiddlewareAuth` deprecated! + + + + +```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', +} +``` + + + + +```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', +} +``` + + + + +### 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 + +``` + +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()) + +// 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({ + 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()) +``` + +Retrieving a supabase client object from the SessionContext: + +```tsx +import { useSupabaseClient } from '@supabase/auth-helpers-react' +import { Database } from '../database.types' + +const supabaseClient = useSupabaseClient() +``` + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/auth/auth-helpers/nextjs-server-components.mdx b/apps/docs/pages/guides/auth/auth-helpers/nextjs-server-components.mdx deleted file mode 100644 index be152b784ce..00000000000 --- a/apps/docs/pages/guides/auth/auth-helpers/nextjs-server-components.mdx +++ /dev/null @@ -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). - -
- -
- -> 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 - - - - - -```sh -npm install @supabase/auth-helpers-nextjs -``` - - - - -```sh -yarn add @supabase/auth-helpers-nextjs -``` - - - - -## 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 - - - - -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 -} -``` - - - - - -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({ 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. - - - - -## Supabase Provider - -All Client Components need to share a single instance of the Supabase client. We can wrap our application in a `` and use React Context to create a global Supabase instance. - - - - -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 ( - - <>{children} - - ) -} - -export const useSupabase = () => { - const context = useContext(Context) - - if (context === undefined) { - throw new Error('useSupabase must be used inside SupabaseProvider') - } - - return context -} -``` - - - - - -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 - session: MaybeSession -} - -const Context = createContext(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 ( - - <>{children} - - ) -} - -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. - - - - - - - -Modify `layout.jsx` to wrap the application with the `` 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 ( - - - - - {children} - - - - ) -} -``` - - - - - -Modify `layout.tsx` to wrap the application with the `` 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 ( - - - - - {children} - - - - ) -} -``` - - - - -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. - - - - -```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 ( - <> - setContent(e.target.value)} value={content} /> - - - ) -} -``` - - - - - -```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 ( - <> - setContent(e.target.value)} value={content} /> - - - ) -} -``` - - - - -> 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 👆 - - - - -```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
{JSON.stringify(data, null, 2)}
-} -``` - -
- - - -```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({ - headers, - cookies, - }) - const { data } = await supabase.from('posts').select('*') - - return
{JSON.stringify(data, null, 2)}
-} -``` - -
-
- -> 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 👆 - - - - -```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) -} -``` - - - - - -```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({ - headers, - cookies, - }) - const { data } = await supabase.from('posts').select('*') - - return NextResponse.json(data) -} -``` - - - - -> 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 }) => - -export default Page diff --git a/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx b/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx index 2a5cb8a96f4..f1cd26dba4a 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx @@ -2,14 +2,32 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { id: 'nextjs', - title: 'Supabase Auth with Next.js', - description: 'Authentication helpers for Next.js API routes, middleware, and SSR.', - sidebar_label: 'Next.js (pages)', + title: 'Supabase Auth with the Next.js App Router', + description: + 'Authentication and Authorization helpers for creating an authenticated Supabase client with the Next.js 13 App Router.', + sidebar_label: 'Next.js', } -This submodule provides convenience helpers for implementing user authentication in Next.js applications. +The [Next.js Auth Helpers package](https://github.com/supabase/auth-helpers) configures Supabase Auth to store the user's `session` in a `cookie`, rather than `localStorage`. This makes it available across the client and server of the App Router - [Client Components](/docs/guides/auth/auth-helpers/nextjs#client-components), [Server Components](/docs/guides/auth/auth-helpers/nextjs#server-components), [Server Actions](/docs/guides/auth/auth-helpers/nextjs#server-actions), [Route Handlers](/docs/guides/auth/auth-helpers/nextjs#route-handlers) and [Middleware](/docs/guides/auth/auth-helpers/nextjs#middleware). The `session` is automatically sent along with any requests to Supabase. -## Install the Next.js helper library +
+ +
+ + + +If you are using the `pages` directory, check out [Auth Helpers in Next.js Pages Directory](/docs/guides/auth/auth-helpers/nextjs-pages). + + + +## Configuration + +### Install Next.js Auth Helpers library + ```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` - -> Note: As of [Next.js 13.4](https://nextjs.org/blog/next-13-4), the `app` directory and Server Components have reached stable status. Check out our guide on using [Auth Helpers with Next.js Server Components](/docs/guides/auth/auth-helpers/nextjs-server-components). - -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 -``` - @@ -43,32 +49,27 @@ npm install @supabase/auth-helpers-react yarn add @supabase/auth-helpers-nextjs ``` -This library supports the following tooling versions: - -- Node.js: `^10.13.0 || >=12.0.0` -- Next.js: `>=10` - -> Note: Next.js 13 is stable, however, the new `app` directory and Server Components are still in beta. Check out our experimental guide on [using Auth Helpers with Next.js Server Components](/docs/guides/auth/auth-helpers/nextjs-server-components). - -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 -``` - -## Set up environment variables +### Declare 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). +Retrieve your project's URL and anon key from your [API settings](https://app.supabase.com/project/_/settings/api), 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 +```bash .env.local +NEXT_PUBLIC_SUPABASE_URL=your-supabase-url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key ``` -## Basic Setup +Make sure you name these variables exactly as seen here, since these are picked up by the auth helpers library — you no longer have to explicitly pass them to a `createClient()` function as before. + +### Managing session with Middleware + +When using the Supabase client on the server, you must perform extra steps to ensure the user's auth session remains active. Since the user's session is tracked in a cookie, we need to read this cookie and update it if necessary. + +In Next.js Server Components, you can read a cookie, but you can't write back to it. Middleware on the other hand, allow you to both read a write to cookies. + +Next.js [Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) runs immediately before each route is rendered. We'll use Middleware to refresh the user's session before loading Server Component routes. -Wrap your `pages/_app.js` component with the `SessionContextProvider` component: +Create a new `middleware.js` file in the root of your project and populate with the following: -```jsx title=pages/_app.js -import { createBrowserSupabaseClient } 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(() => createBrowserSupabaseClient()) - - return ( - - - - ) -} -``` - - - - -Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component: - -```tsx lines=2,8 title=pages/_app.tsx -import { createBrowserSupabaseClient } 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(() => createBrowserSupabaseClient()) - - return ( - - - - ) -} -``` - - - - -You can now determine if a user is authenticated by checking that the `user` object returned by the `useUser()` hook is defined. - -## 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 { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' -import { Database } from '../database.types' - -const supabaseClient = createBrowserSupabaseClient() -``` - -Retrieving a supabase client object from the SessionContext: - -```tsx -import { useSupabaseClient } from '@supabase/auth-helpers-react' -import { Database } from '../database.types' - -const supabaseClient = useSupabaseClient() -``` - -### Server client - -```tsx -// Creating a new supabase server client object (e.g. in API route): -import { createServerSupabaseClient } 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 = createServerSupabaseClient({ - 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 lines=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 ( - - ) - - return ( - <> - -

user:

-
{JSON.stringify(user, null, 2)}
-

client-side data fetching with RLS

-
{JSON.stringify(data, null, 2)}
- - ) -} - -export default LoginPage -``` - -## Server-side rendering (SSR) - -Create a server supabase client to retrieve the logged in user's session: - -```jsx title=pages/profile.js -import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' - -export default function Profile({ user }) { - return
Hello {user.name}
-} - -export const getServerSideProps = async (ctx) => { - // Create authenticated Supabase Client - const supabase = createServerSupabaseClient(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: - - - - -```jsx -import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' - -export default function ProtectedPage({ user, data }) { - return ( - <> -
Protected content for {user.email}
-
{JSON.stringify(data, null, 2)}
-
{JSON.stringify(user, null, 2)}
- - ) -} - -export const getServerSideProps = async (ctx) => { - // Create authenticated Supabase Client - const supabase = createServerSupabaseClient(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 ?? [], - }, - } -} -``` - -
- - -```tsx -import { User, createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' -import { GetServerSidePropsContext } from 'next' - -export default function ProtectedPage({ user, data }: { user: User; data: any }) { - return ( - <> -
Protected content for {user.email}
-
{JSON.stringify(data, null, 2)}
-
{JSON.stringify(user, null, 2)}
- - ) -} - -export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { - // Create authenticated Supabase Client - const supabase = createServerSupabaseClient(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 ?? [], - }, - } -} -``` - -
-
- -## 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. - - - - -```jsx -import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' - -export default function ProtectedPage({ user, allRepos }) { - return ( - <> -
Protected content for {user.email}
-

Data fetched with provider token:

-
{JSON.stringify(allRepos, null, 2)}
-

user:

-
{JSON.stringify(user, null, 2)}
- - ) -} - -export const getServerSideProps = async (ctx) => { - // Create authenticated Supabase Client - const supabase = createServerSupabaseClient(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 } } -} -``` - -
- - -```tsx -import { User, createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' -import { GetServerSidePropsContext } from 'next' - -export default function ProtectedPage({ user, allRepos }: { user: User; allRepos: any }) { - return ( - <> -
Protected content for {user.email}
-

Data fetched with provider token:

-
{JSON.stringify(allRepos, null, 2)}
-

user:

-
{JSON.stringify(user, null, 2)}
- - ) -} - -export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { - // Create authenticated Supabase Client - const supabase = createServerSupabaseClient(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 } } -} -``` - -
-
- -## Protecting API routes - -Create a server supabase client to retrieve the logged in user's session: - - - - -```jsx title=pages/api/protected-route.js -import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' - -const ProtectedRoute = async (req, res) => { - // Create authenticated Supabase Client - const supabase = createServerSupabaseClient({ 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 -``` - - - - -```tsx title=pages/api/protected-route.ts -import { NextApiHandler } from 'next' -import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' - -const ProtectedRoute: NextApiHandler = async (req, res) => { - // Create authenticated Supabase Client - const supabase = createServerSupabaseClient({ 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 -``` - - - - -## 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 title=middleware.ts -import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs' +```js middleware.js +import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs' import { NextResponse } from 'next/server' + +export async function middleware(req) { + const res = NextResponse.next() + const supabase = createMiddlewareClient({ req, res }) + await supabase.auth.getSession() + return res +} +``` + + + + + +Create a new `middleware.ts` file in the root of your project and populate with the following: + +```ts middleware.ts +import { createMiddlewareClient } 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) { - // 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 = createMiddlewareSupabaseClient({ 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*', + const supabase = createMiddlewareClient({ req, res }) + await supabase.auth.getSession() + return res } ``` + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createMiddlewareClient` to add type support to the Supabase client. + + + + + + + + +The `getSession` function must be called for any Server Component routes that use a Supabase client. + + + +### Managing sign-in with Code Exchange + +The Next.js Auth Helpers are configured to use the [server-side auth flow](/docs/guides/auth/server-side-rendering) to sign users into your application. This requires you to setup a `Code Exchange` route, to exchange an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase. + +To make this work with Next.js, we create a callback Route Handler that performs this exchange: + + + + +Create a new file at `app/auth/callback/route.js` and populate with the following: + +```js app/auth/callback/route.js +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' + +export async function GET(request) { + const requestUrl = new URL(request.url) + const code = requestUrl.searchParams.get('code') + + if (code) { + const supabase = createRouteHandlerClient({ cookies }) + await supabase.auth.exchangeCodeForSession(code) + } + + // URL to redirect to after sign in process completes + return NextResponse.redirect(requestUrl.origin) +} +``` + + + + + +Create a new file at `app/auth/callback/route.ts` and populate with the following: + +```ts app/auth/callback/route.ts +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' + +import type { NextRequest } from 'next/server' +import type { Database } from '@/lib/database.types' + +export async function GET(request: NextRequest) { + const requestUrl = new URL(request.url) + const code = requestUrl.searchParams.get('code') + + if (code) { + const supabase = createRouteHandlerClient({ cookies }) + await supabase.auth.exchangeCodeForSession(code) + } + + // URL to redirect to after sign in process completes + return NextResponse.redirect(requestUrl.origin) +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. + + + + + + +## Authentication + +
+ +
+ +Authentication can be initiated [client](/docs/guides/auth/auth-helpers/nextjs#client-side) or [server-side](/docs/guides/auth/auth-helpers/nextjs#server-side). All of the [supabase-js authentication strategies](/docs/reference/javascript/auth-api) are supported with the Auth Helpers client. + + + +The authentication flow requires the [Code Exchange Route](/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange) to exchange a `code` for the user's `session`. + + + +### Client-side + +Client Components can be used to trigger the authentication process from event handlers. + + + + +```jsx app/login.jsx +'use client' + +import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' +import { useRouter } from 'next/navigation' +import { useState } from 'react' + +export default function Login() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const router = useRouter() + const supabase = createClientComponentClient() + + const handleSignUp = async () => { + await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${location.origin}/auth/callback`, + }, + }) + router.refresh() + } + + const handleSignIn = async () => { + await supabase.auth.signInWithPassword({ + email, + password, + }) + router.refresh() + } + + const handleSignOut = async () => { + await supabase.auth.signOut() + router.refresh() + } + + return ( + <> + setEmail(e.target.value)} value={email} /> + setPassword(e.target.value)} + value={password} + /> + + + + + ) +} +``` + + + + + +```tsx app/login.tsx +'use client' + +import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' +import { useRouter } from 'next/navigation' +import { useState } from 'react' + +import type { Database } from '@/lib/database.types' + +export default function Login() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const router = useRouter() + const supabase = createClientComponentClient() + + const handleSignUp = async () => { + await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${location.origin}/auth/callback`, + }, + }) + router.refresh() + } + + const handleSignIn = async () => { + await supabase.auth.signInWithPassword({ + email, + password, + }) + router.refresh() + } + + const handleSignOut = async () => { + await supabase.auth.signOut() + router.refresh() + } + + return ( + <> + setEmail(e.target.value)} value={email} /> + setPassword(e.target.value)} + value={password} + /> + + + + + ) +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createClientComponentClient` to add type support to the Supabase client. + + + + + + +### Server-side + +The combination of [Server Components](https://nextjs.org/docs/getting-started/react-essentials#server-components) and [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions) can be used to trigger the authentication process from form submissions. + + + +Next.js [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions) are currently in Alpha and may change. For now we recommend [triggering the authentication flow client-side](/docs/guides/auth/auth-helpers/nextjs#client-side) for production applications. + + + + + + +```jsx app/login.js +import { createServerActionClient } from '@supabase/auth-helpers-nextjs' +import { revalidatePath } from 'next/cache' +import { cookies } from 'next/headers' + +export default async function Login() { + const handleSignUp = async (formData) => { + 'use server' + const email = formData.get('email') + const password = formData.get('password') + + const supabase = createServerActionClient({ cookies }) + await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: 'http://localhost:3000/auth/callback', + }, + }) + + revalidatePath('/') + } + + const handleSignIn = async (formData) => { + 'use server' + const email = formData.get('email') + const password = formData.get('password') + + const supabase = createServerActionClient({ cookies }) + await supabase.auth.signInWithPassword({ + email, + password, + }) + + revalidatePath('/') + } + + const handleSignOut = async () => { + 'use server' + const supabase = createServerActionClient({ cookies }) + await supabase.auth.signOut() + revalidatePath('/') + } + + return ( +
+ + + + + +
+ ) +} +``` + +
+ + + +```tsx app/login.ts +import { createServerActionClient } from '@supabase/auth-helpers-nextjs' +import { revalidatePath } from 'next/cache' +import { cookies } from 'next/headers' + +import type { Database } from '@/lib/database.types' + +export default async function Login() { + const handleSignUp = async (formData: FormData) => { + 'use server' + const email = String(formData.get('email')) + const password = String(formData.get('password')) + + const supabase = createServerActionClient({ cookies }) + await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: 'http://localhost:3000/auth/callback', + }, + }) + + revalidatePath('/') + } + + const handleSignIn = async (formData: FormData) => { + 'use server' + const email = String(formData.get('email')) + const password = String(formData.get('password')) + + const supabase = createServerActionClient({ cookies }) + await supabase.auth.signInWithPassword({ + email, + password, + }) + + revalidatePath('/') + } + + const handleSignOut = async () => { + 'use server' + const supabase = createServerActionClient({ cookies }) + await supabase.auth.signOut() + revalidatePath('/') + } + + return ( +
+ + + + + +
+ ) +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createServerActionClient` to add type support to the Supabase client. + + + +
+
+ +## Creating a Supabase Client + +There are 5 ways to access the Supabase client with the Next.js Auth Helpers: + +- [Client Components](/docs/guides/auth/auth-helpers/nextjs#client-components) — `createClientComponentClient` in Client Components +- [Server Components](/docs/guides/auth/auth-helpers/nextjs#server-components) — `createServerComponentClient` in Server Components +- [Server Actions](/docs/guides/auth/auth-helpers/nextjs#server-actions) — `createServerActionClient` in Server Actions +- [Route Handlers](/docs/guides/auth/auth-helpers/nextjs#route-handlers) — `createRouteHandlerClient` in Route Handlers +- [Middleware](/docs/guides/auth/auth-helpers/nextjs#middleware) — `createMiddlewareClient` in Middleware + +This allows for the Supabase client to be easily instantiated in the correct context. All you need to change is the context in the middle `create[ClientComponent|ServerComponent|ServerAction|RouteHandler|Middleware]Client` and the Auth Helpers will take care of the rest. + +### Client Components + +
+ +
+ +[Client Components](https://nextjs.org/docs/getting-started/react-essentials#client-components) allow the use of client-side hooks - such as `useEffect` and `useState`. They can be used to request data from Supabase client-side, and [subscribe to realtime events](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx). + + + + +```jsx app/client/page.jsx +'use client' + +import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' +import { useEffect, useState } from 'react' + +export default function Home() { + const [todos, setTodos] = useState() + const supabase = createClientComponentClient() + + useEffect(() => { + const getData = async () => { + const { data } = await supabase.from('todos').select() + setTodos(data) + } + + getData() + }, []) + + return todos ?
{JSON.stringify(todos, null, 2)}
:

Loading todos...

+} +``` + +
+ + + +```tsx app/client/page.tsx +'use client' + +import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' +import { useEffect, useState } from 'react' + +import type { Database } from '@/lib/database.types' + +type Todo = Database['public']['Tables']['todos']['Row'] + +export default function Home() { + const [todos, setTodos] = useState(null) + const supabase = createClientComponentClient() + + useEffect(() => { + const getData = async () => { + const { data } = await supabase.from('todos').select() + setTodos(data) + } + + getData() + }, []) + + return todos ?
{JSON.stringify(todos, null, 2)}
:

Loading todos...

+} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createClientComponentClient` to add type support to the Supabase client. + + + +
+
+ + + +Check out the [Next.js auth example repo](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) for more examples, including [realtime subscriptions](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx). + + + +#### Singleton + +The `createClientComponentClient` function implements a [Singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern) by default, meaning that all invocations will return the same Supabase client instance. If you need multiple Supabase instances across Client Components, you can pass an additional configuration option `{ isSingleton: false }` to get a new client every time this function is called. + +```jsx +const supabase = createClientComponentClient({ isSingleton: false }) +``` + +### Server Components + +
+ +
+ +[Server Components](https://nextjs.org/docs/getting-started/react-essentials#server-components) allow for asynchronous data to be fetched server-side. + + + +In order to use Supabase in Server Components, you need to have implemented the [Middleware](/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware) steps above. + + + + + + +```jsx app/page.jsx +import { cookies } from 'next/headers' +import { createServerComponentClient } from '@supabase/auth-helpers-nextjs' + +export default async function Home() { + const supabase = createServerComponentClient({ cookies }) + const { data } = await supabase.from('todos').select() + return
{JSON.stringify(data, null, 2)}
+} +``` + +
+ + + +```tsx app/page.tsx +import { cookies } from 'next/headers' +import { createServerComponentClient } from '@supabase/auth-helpers-nextjs' + +import type { Database } from '@/lib/database.types' + +export default async function ServerComponent() { + const supabase = createServerComponentClient({ cookies }) + const { data } = await supabase.from('todos').select() + return
{JSON.stringify(data, null, 2)}
+} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createServerComponentClient` to add type support to the Supabase client. + + + +
+
+ + + +Check out the [Next.js auth example repo](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) for more examples, including redirecting unauthenticated users - [protected pages](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/[id]/page.tsx). + + + +### Server Actions + +
+ +
+ +[Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions) allow mutations to be performed server-side. + + + +Next.js Server Actions are currently in `alpha` so may change without notice. + + + + + + +```jsx app/new-post.jsx +import { cookies } from 'next/headers' +import { createServerActionClient } from '@supabase/auth-helpers-nextjs' +import { revalidatePath } from 'next/cache' + +export default async function NewTodo() { + const addTodo = async (formData) => { + 'use server' + + const title = formData.get('title') + const supabase = createServerActionClient({ cookies }) + await supabase.from('todos').insert({ title }) + revalidatePath('/') + } + + return ( +
+ +
+ ) +} +``` + +
+ + + +```tsx app/new-post.tsx +import { cookies } from 'next/headers' +import { createServerActionClient } from '@supabase/auth-helpers-nextjs' +import { revalidatePath } from 'next/cache' + +import type { Database } from '@/lib/database.types' + +export default async function NewTodo() { + const addTodo = async (formData: FormData) => { + 'use server' + + const title = formData.get('title') + const supabase = createServerActionClient({ cookies }) + await supabase.from('todos').insert({ title }) + revalidatePath('/') + } + + return ( +
+ +
+ ) +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createServerActionClient` to add type support to the Supabase client. + + + +
+
+ +### Route Handlers + +
+ +
+ +[Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) replace API Routes and allow for logic to be performed server-side. They can respond to `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, and `OPTIONS` requests. + + + + +```jsx app/api/todos/route.jsx +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { NextResponse } from 'next/server' +import { cookies } from 'next/headers' + +export async function POST(request) { + const { title } = await request.json() + const supabase = createRouteHandlerClient({ cookies }) + const { data } = await supabase.from('todos').insert({ title }).select() + return NextResponse.json(data) +} +``` + + + + + +```tsx app/api/todos/route.tsx +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { NextResponse } from 'next/server' +import { cookies } from 'next/headers' + +import type { Database } from '@/lib/database.types' + +export async function POST(request: Request) { + const { title } = await request.json() + const supabase = createRouteHandlerClient({ cookies }) + const { data } = await supabase.from('todos').insert({ title }).select() + return NextResponse.json(data) +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. + + + + + + +### Middleware + +See [refreshing session example](/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware) above. + +## More examples + +- [Cookie-based Auth and the Next.js 13 App Router (free course)](https://youtube.com/playlist?list=PL5S4mPUpp4OtMhpnp93EFSo42iQ40XjbF) +- [Full App Router example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) +- [Realtime Subscriptions](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx) +- [Protected Routes](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/[id]/page.tsx) +- [Conditional Rendering in Client Components with SSR](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login-form.tsx) + ## Migration Guide -### Migrating to v0.5.X +### Migrating to v0.7.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). +#### PKCE Auth Flow -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). +PKCE is the new server-side auth flow implemented by the Next.js Auth Helpers. It requires a new Route Handler for `/auth/callback` that exchanges an auth `code` for the user's `session`. -Please follow the steps below to update your API routes, pages, and middleware handlers. Thanks! +Check the [Code Exchange Route steps](/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange) above to implement this Route Handler. -#### `withApiAuth` deprecated! +#### Authentication -Use `createServerSupabaseClient` within your `NextApiHandler`: - - - - -```tsx title=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) -}) -``` - - - - -```tsx title=pages/api/protected-route.ts -import { NextApiHandler } from 'next' -import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs' - -const ProtectedRoute: NextApiHandler = async (req, res) => { - // Create authenticated Supabase Client - const supabase = createServerSupabaseClient({ 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 -``` - - - - -#### `withPageAuth` deprecated! - -Use `createServerSupabaseClient` within `getServerSideProps`: - - - - -```tsx title=pages/profile.tsx -import { withPageAuth, User } from '@supabase/auth-helpers-nextjs' - -export default function Profile({ user }: { user: User }) { - return
{JSON.stringify(user, null, 2)}
-} - -export const getServerSideProps = withPageAuth({ redirectTo: '/' }) -``` - -
- - -```tsx title=pages/profile.js -import { createServerSupabaseClient, User } from '@supabase/auth-helpers-nextjs' -import { GetServerSidePropsContext } from 'next' - -export default function Profile({ user }: { user: User }) { - return
{JSON.stringify(user, null, 2)}
-} - -export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { - // Create authenticated Supabase Client - const supabase = createServerSupabaseClient(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, - }, - } -} -``` - -
-
- -#### `withMiddlewareAuth` deprecated! - - - - -```tsx title=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', -} -``` - - - - -```tsx title=middleware.ts -import { createMiddlewareSupabaseClient } 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 = createMiddlewareSupabaseClient({ 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', -} -``` - - - - -### 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: +For authentication methods that have a `redirectTo` or `emailRedirectTo`, this must be set to this new code exchange Route Handler - `/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', + }, +}) ``` -The `supabaseClient` and `supabaseServerClient` have been removed in favor of the `createBrowserSupabaseClient` and `createServerSupabaseClient` methods. This allows you to provide the CLI-generated types to the client: +#### Deprecated Functions -```tsx -// client-side -import type { Database } from 'types_db' -const [supabaseClient] = useState(() => createBrowserSupabaseClient()) +With v0.7.x of the Next.js Auth Helpers a new naming convention has been implemented for createClient functions. The `createMiddlewareSupabaseClient`, `createBrowserSupabaseClient`, `createServerComponentSupabaseClient` and `createRouteHandlerSupabaseClient` functions have been marked as deprecated, and will be removed in a future version of the Auth Helpers. -// server-side API route -import type { NextApiRequest, NextApiResponse } from 'next' -import type { Database } from 'types_db' +- `createMiddlewareSupabaseClient` has been replaced with `createMiddlewareClient` +- `createBrowserSupabaseClient` has been replaced with `createClientComponentClient` +- `createServerComponentSupabaseClient` has been replaced with `createServerComponentClient` +- `createRouteHandlerSupabaseClient` has been replaced with `createRouteHandlerClient` -export default async (req: NextApiRequest, res: NextApiResponse) => { - const supabaseServerClient = createServerSupabaseClient({ - req, - res, - }) - const { - data: { user }, - } = await supabaseServerClient.auth.getUser() +#### createClientComponentClient returns singleton - res.status(200).json({ name: user?.name ?? '' }) +You no longer need to implement logic to ensure there is only a single instance of the Supabase Client shared across all Client Components - this is now the default and handled by the `createClientComponentClient` function. Call it as many times as you want! + +```jsx +"use client"; + +import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"; + +export default function() { + const supabase = createClientComponentClient(); + return ... } ``` -- 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(() => createBrowserSupabaseClient()) -``` - -Retrieving a supabase client object from the SessionContext: - -```tsx -import { useSupabaseClient } from '@supabase/auth-helpers-react' -import { Database } from '../database.types' - -const supabaseClient = useSupabaseClient() -``` +For an example of creating multiple Supabase clients, check [Singleton section](/docs/guides/auth/auth-helpers/nextjs#singleton) above. export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/auth/auth-helpers/remix.mdx b/apps/docs/pages/guides/auth/auth-helpers/remix.mdx index 7ff90a03904..80b91640f8e 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/remix.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/remix.mdx @@ -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. + + + + +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, + }) +} +``` + + + + + +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( + 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). + + + + ## 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 > -```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 ``` -```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() ``` 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(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 ``` @@ -341,7 +417,7 @@ Let's pipe that through from our loader. -```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 }) => { -```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. -```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]) ``` -```tsx title=app/root.tsx +```tsx app/root.tsx const { env, session } = useLoaderData() const { revalidate } = useRevalidator() @@ -471,7 +547,7 @@ useEffect(() => { return () => { subscription.unsubscribe() } -}, [serverAccessToken, supabase, fetcher]) +}, [serverAccessToken, supabase, revalidate]) ``` @@ -493,7 +569,7 @@ Now we can use our outlet context to access our single instance of Supabase and -```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() { -```tsx title=app/components/login.tsx +```tsx app/components/login.tsx export default function Login() { const { supabase } = useOutletContext<{ supabase: SupabaseClient }>() @@ -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() { -```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() { -```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 }) => export default Page diff --git a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx index 13e109beae9..8137d96df44 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx @@ -9,31 +9,77 @@ export const meta = { This submodule provides convenience helpers for implementing user authentication in [SvelteKit](https://kit.svelte.dev/) applications. -## Installation +## Configuration + +### Install SvelteKit Auth Helpers library This library supports Node.js `^16.15.0`. -```sh +```sh Terminal npm install @supabase/auth-helpers-sveltekit ``` -## Getting Started +### Declare Environment Variables -### Configuration +Retrieve your project's URL and anon key from your [API settings](https://app.supabase.com/project/_/settings/api), and create a `.env.local` file with the following environment variables: -Set up the following env vars. For local development you can set them in a `.env` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/sveltekit/.env.example). - -```bash +```bash .env.local # Find these in your Supabase project settings https://app.supabase.com/project/_/settings/api PUBLIC_SUPABASE_URL=https://your-project.supabase.co PUBLIC_SUPABASE_ANON_KEY=your-anon-key ``` -### Set up the Supabase client +### Creating a Supabase Client -Create a server supabase client in a handle hook: + + -```ts title=src/hooks.server.ts +Create a new `hooks.server.js` file in the root of your project and populate with the following: + +```js src/hooks.server.js +// src/hooks.server.js +import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' +import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit' + +export const handle = async ({ event, resolve }) => { + event.locals.supabase = createSupabaseServerClient({ + supabaseUrl: PUBLIC_SUPABASE_URL, + supabaseKey: PUBLIC_SUPABASE_ANON_KEY, + event, + }) + + /** + * a little helper that is written for convenience so that instead + * of calling `const { data: { session } } = await supabase.auth.getSession()` + * you just call this `await getSession()` + */ + event.locals.getSession = async () => { + const { + data: { session }, + } = await event.locals.supabase.auth.getSession() + return session + } + + return resolve(event, { + filterSerializedResponseHeaders(name) { + return name === 'content-range' + }, + }) +} +``` + + + + + +Create a new `hooks.server.ts` file in the root of your project and populate with the following: + +```ts src/hooks.server.ts // src/hooks.server.ts import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit' @@ -59,11 +105,6 @@ export const handle: Handle = async ({ event, resolve }) => { } return resolve(event, { - /** - * There´s an issue with `filterSerializedResponseHeaders` not working when using `sequence` - * - * https://github.com/sveltejs/kit/issues/8061 - */ filterSerializedResponseHeaders(name) { return name === 'content-range' }, @@ -71,96 +112,71 @@ export const handle: Handle = async ({ event, resolve }) => { } ``` -> Note that we are specifying filterSerializedResponseHeaders here. We need to tell SvelteKit that supabase needs the content-range header. + + -### Send session to client + -In order to make the session available to the UI (pages, layouts) we need to pass the session in the root layout server load function: +Note that we are specifying filterSerializedResponseHeaders here. We need to tell SvelteKit that supabase needs the content-range header. -```ts title=src/routes/+layout.server.ts -// src/routes/+layout.server.ts -import type { LayoutServerLoad } from './$types' + -export const load: LayoutServerLoad = async ({ locals: { getSession } }) => { - return { - session: await getSession(), +### 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 SvelteKit Auth Helpers. It exchanges an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase. + + + + +Create a new file at `src/routes/auth/callback/+server.js` and populate with the following: + +```js src/routes/auth/callback/+server.js +import { redirect } from '@sveltejs/kit' + +export const GET = async ({ url, locals: { supabase } }) => { + const code = url.searchParams.get('code') + + if (code) { + await supabase.auth.exchangeCodeForSession(code) } + + throw redirect(303, '/') } ``` -### Shared Load functions and pages + -To be able to use Supabase in shared load functions and inside pages you need to create a Supabase client in the root layout load: + -```ts -// src/routes/+layout.ts -import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' -import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit' -import type { LayoutLoad } from './$types' -import type { Database } from '../DatabaseDefinitions' +Create a new file at `src/routes/auth/callback/+server.ts` and populate with the following: -export const load: LayoutLoad = async ({ fetch, data, depends }) => { - depends('supabase:auth') +```ts src/routes/auth/callback/+server.ts +import { redirect } from '@sveltejs/kit' - const supabase = createSupabaseLoadClient({ - supabaseUrl: PUBLIC_SUPABASE_URL, - supabaseKey: PUBLIC_SUPABASE_ANON_KEY, - event: { fetch }, - serverSession: data.session, - }) +export const GET = async ({ url, locals: { supabase } }) => { + const code = url.searchParams.get('code') - const { - data: { session }, - } = await supabase.auth.getSession() + if (code) { + await supabase.auth.exchangeCodeForSession(code) + } - return { supabase, session } + throw redirect(303, '/') } ``` -Access the client inside pages by `$page.data.supabase` or `data.supabase` when using `export let data: PageData`. - -The usage of `depends` tells sveltekit that this load function should be executed whenever `invalidate` is called to keep the page store in sync. - -`createSupabaseLoadClient` caches the client when running in a browser environment and therefore does not create a new client for every time the load function runs. - -### Setting up the event listener on the client side - -We need to create an event listener in the root `+layout.svelte` file in order catch supabase events being triggered. - -```svelte - - - - -``` - -The usage of `invalidate` tells sveltekit that the root `+layout.ts` load function should be executed whenever the session updates to keep the page store in sync. + + ### Generate types from your database In order to get the most out of TypeScript and it's intellisense, you should import the generated Database types into the `app.d.ts` type definition file that comes with your SvelteKit project, where `import('./DatabaseDefinitions')` points to the generated types file outlined in [v2 docs here](https://supabase.com/docs/reference/javascript/release-notes#typescript-support) after you have logged in, linked, and generated types through the Supabase CLI. -```ts +```ts src/app.d.ts // src/app.d.ts import { SupabaseClient, Session } from '@supabase/supabase-js' @@ -181,78 +197,325 @@ declare global { } ``` -## Client-side data fetching with RLS +## Authentication -For [row level security](https://supabase.com/docs/guides/auth/row-level-security) to work properly when fetching data client-side, you need to use `supabaseClient` from `PageData` and only run your query once the session is defined client-side: +Authentication can be initiated [client](/docs/guides/auth/auth-helpers/sveltekit#client-side) or [server-side](/docs/guides/auth/auth-helpers/sveltekit#server-side). All of the [supabase-js authentication strategies](/docs/reference/javascript/auth-api) are supported with the Auth Helpers client. -```html - +### Client-side -{#if data.session} -

client-side data fetching with RLS

-
{JSON.stringify(loadedData, null, 2)}
-{/if} -``` +#### Send session to client -## Server-side data fetching with RLS +To make the session available across the UI, including pages and layouts, it is crucial to pass the session as a parameter in the root layout's server load function. -```html - - - -
Protected content for {user.email}
-
{JSON.stringify(tableData, null, 2)}
-
{JSON.stringify(user, null, 2)}
-``` - -```ts -// src/routes/profile/+page.ts -import type { PageLoad } from './$types' -import { redirect } from '@sveltejs/kit' - -export const load: PageLoad = async ({ parent }) => { - const { supabase, session } = await parent() - if (!session) { - throw redirect(303, '/') - } - const { data: tableData } = await supabase.from('test').select('*') + + +```js src/routes/+layout.server.js +// src/routes/+layout.server.js +export const load = async ({ locals: { getSession } }) => { return { - user: session.user, - tableData, + session: await getSession(), } } ``` -## Protecting API routes + + + +```ts src/routes/+layout.server.ts +// src/routes/+layout.server.ts +export const load = async ({ locals: { getSession } }) => { + return { + session: await getSession(), + } +} +``` + + + + +#### Shared Load functions and pages + +To utilize Supabase in shared load functions and within pages, it is essential to create a Supabase client in the root layout load. + + + + +```ts src/routes/+layout.js +// src/routes/+layout.js +import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' +import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit' + +export const load = async ({ fetch, data, depends }) => { + depends('supabase:auth') + + const supabase = createSupabaseLoadClient({ + supabaseUrl: PUBLIC_SUPABASE_URL, + supabaseKey: PUBLIC_SUPABASE_ANON_KEY, + event: { fetch }, + serverSession: data.session, + }) + + const { + data: { session }, + } = await supabase.auth.getSession() + + return { supabase, session } +} +``` + + + + + +```ts src/routes/+layout.ts +// src/routes/+layout.ts +import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' +import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit' +import type { Database } from '../DatabaseDefinitions' + +export const load = async ({ fetch, data, depends }) => { + depends('supabase:auth') + + const supabase = createSupabaseLoadClient({ + supabaseUrl: PUBLIC_SUPABASE_URL, + supabaseKey: PUBLIC_SUPABASE_ANON_KEY, + event: { fetch }, + serverSession: data.session, + }) + + const { + data: { session }, + } = await supabase.auth.getSession() + + return { supabase, session } +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createSupabaseLoadClient` to add type support to the Supabase client. + + + + + + +Access the client inside pages by `$page.data.supabase` or `data.supabase` when using `export let data`. + +The usage of `depends` tells sveltekit that this load function should be executed whenever `invalidate` is called to keep the page store in sync. + +`createSupabaseLoadClient` caches the client when running in a browser environment and therefore does not create a new client for every time the load function runs. + +#### Setting up the event listener on the client side + +We need to create an event listener in the root `+layout.svelte` file in order to catch supabase events being triggered. + +```svelte src/routes/+layout.svelte + + + + +``` + +The usage of `invalidate` tells SvelteKit that the root `+layout.ts` load function should be executed whenever the session updates to keep the page store in sync. + +#### Sign in / Sign up / Sign out + +We can access the supabase instance in our `+page.svelte` file through the data object. + +```svelte src/routes/auth/+page.svelte + + + +
+ + + +
+ + + +``` + +### Server-side + +[Form Actions](https://kit.svelte.dev/docs/form-actions) can be used to trigger the authentication process from form submissions. + + + + +```js src/routes/login/+page.server.js +// src/routes/login/+page.server.js +export const actions = { + default: async ({ request, url, locals: { supabase } }) => { + const formData = await request.formData() + const email = formData.get('email') + const password = formData.get('password') + + const { error } = await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${url.origin}/auth/callback`, + }, + }) + + if (error) { + return fail(500, { message: 'Server error. Try again later.', success: false, email }) + } + + return { + message: 'Please check your email for a magic link to log into the website.', + success: true, + } + }, +} +``` + +```svelte src/routes/login/+page.svelte + + + +
+ + + +
+``` + +
+ + + +```js src/routes/login/+page.server.ts +// src/routes/login/+page.server.ts +export const actions = { + default: async ({ request, url, locals: { supabase } }) => { + const formData = await request.formData() + const email = formData.get('email') as string + const password = formData.get('password') as string + + const { error } = await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${url.origin}/auth/callback`, + }, + }) + + if (error) { + return fail(500, { message: 'Server error. Try again later.', success: false, email }) + } + + return { + message: 'Please check your email for a magic link to log into the website.', + success: true, + } + }, +} +``` + +```svelte src/routes/login/+page.svelte + + + +
+ + + +
+``` + +
+
+ +## Authorization + +### Protecting API routes Wrap an API Route to check that the user has a valid session. If they're not logged in the session is `null`. -```ts +```ts src/routes/api/protected-route/+server.ts // src/routes/api/protected-route/+server.ts -import type { RequestHandler } from './$types' import { json, error } from '@sveltejs/kit' -export const GET: RequestHandler = async ({ locals: { supabase, getSession } }) => { +export const GET = async ({ locals: { supabase, getSession } }) => { const session = await getSession() if (!session) { // the user is not signed in @@ -266,16 +529,15 @@ export const GET: RequestHandler = async ({ locals: { supabase, getSession } }) If you visit `/api/protected-route` without a valid session cookie, you will get a 401 response. -## Protecting Actions +### Protecting Actions Wrap an Action to check that the user has a valid session. If they're not logged in the session is `null`. -```ts +```ts src/routes/posts/+page.server.ts // src/routes/posts/+page.server.ts -import type { Actions } from './$types' import { error, fail } from '@sveltejs/kit' -export const actions: Actions = { +export const actions = { createPost: async ({ request, locals: { supabase, getSession } }) => { const session = await getSession() @@ -305,14 +567,148 @@ export const actions: Actions = { If you try to submit a form with the action `?/createPost` without a valid session cookie, you will get a 401 error response. +### Protecting multiple routes + +To avoid writing the same auth logic in every single route you can use the handle hook to +protect multiple routes at once. + + + + +```js src/hooks.server.js +// src/hooks.server.js +import { redirect, error } from '@sveltejs/kit' + +export const handle = async ({ event, resolve }) => { + // protect requests to all routes that start with /protected-routes + if (event.url.pathname.startsWith('/protected-routes')) { + const session = await event.locals.getSession() + if (!session) { + // the user is not signed in + throw redirect(303, '/') + } + } + + // protect POST requests to all routes that start with /protected-posts + if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') { + const session = await event.locals.getSession() + if (!session) { + // the user is not signed in + throw error(303, '/') + } + } + + return resolve(event) +} +``` + + + + + +```ts src/hooks.server.ts +// src/hooks.server.ts +import { type Handle, redirect, error } from '@sveltejs/kit' + +export const handle: Handle = async ({ event, resolve }) => { + // protect requests to all routes that start with /protected-routes + if (event.url.pathname.startsWith('/protected-routes')) { + const session = await event.locals.getSession() + if (!session) { + // the user is not signed in + throw redirect(303, '/') + } + } + + // protect POST requests to all routes that start with /protected-posts + if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') { + const session = await event.locals.getSession() + if (!session) { + // the user is not signed in + throw error(303, '/') + } + } + + return resolve(event) +} +``` + + + + +## Data fetching + +### Client-side data fetching with RLS + +For [row level security](https://supabase.com/docs/guides/auth/row-level-security) to work properly when fetching data client-side, you need to use `supabaseClient` from `PageData` and only run your query once the session is defined client-side: + +```svelte src/routes/+page.svelte + + +{#if data.session} +

client-side data fetching with RLS

+
{JSON.stringify(loadedData, null, 2)}
+{/if} +``` + +### Server-side data fetching with RLS + +```svelte src/routes/profile/+page.svelte + + + +
Protected content for {user.email}
+
{JSON.stringify(tableData, null, 2)}
+
{JSON.stringify(user, null, 2)}
+``` + +```ts src/routes/profile/+page.ts +// src/routes/profile/+page.ts +import { redirect } from '@sveltejs/kit' + +export const load = async ({ parent }) => { + const { supabase, session } = await parent() + if (!session) { + throw redirect(303, '/') + } + const { data: tableData } = await supabase.from('test').select('*') + + return { + user: session.user, + tableData, + } +} +``` + ## Saving and deleting the session ```ts -import type { Actions } from './$types' import { fail, redirect } from '@sveltejs/kit' import { AuthApiError } from '@supabase/supabase-js' -export const actions: Actions = { +export const actions = { signin: async ({ request, locals: { supabase } }) => { const formData = await request.formData() @@ -351,42 +747,33 @@ export const actions: Actions = { } ``` -## Protecting multiple routes +## Migration Guide [#migration] -To avoid writing the same auth logic in every single route you can use the handle hook to -protect multiple routes at once. +### Migrate to 0.10 + +#### PKCE Auth Flow + +Proof Key for Code Exchange (PKCE) is the new server-side auth flow implemented by the SvelteKit Auth Helpers. It requires a server endpoint for `/auth/callback` that exchanges an auth `code` for the user's `session`. + +Check the [Code Exchange Route steps](/docs/guides/auth/auth-helpers/sveltekit#code-exchange-route) above to implement this server endpoint. + +#### Authentication + +For authentication methods that have a `redirectTo` or `emailRedirectTo`, this must be set to this new code exchange route handler - `/auth/callback`. This is an example with the `signUp` function: ```ts -// src/hooks.server.ts -import type { RequestHandler } from './$types' -import { redirect, error } from '@sveltejs/kit' - -export const handle: Handle = async ({ event, resolve }) => { - // protect requests to all routes that start with /protected-routes - if (event.url.pathname.startsWith('/protected-routes')) { - const session = await event.locals.getSession() - if (!session) { - // the user is not signed in - throw redirect(303, '/') - } - } - - // protect POST requests to all routes that start with /protected-posts - if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') { - const session = await event.locals.getSession() - if (!session) { - // the user is not signed in - throw error(303, '/') - } - } - - return resolve(event) -} +await supabase.auth.signUp({ + email: 'jon@example.com', + password: 'sup3rs3cur3', + options: { + emailRedirectTo: 'http://localhost:3000/auth/callback', + }, +}) ``` -## Migrate from 0.8.x to 0.9 [#migration] +### Migrate from 0.8.x to 0.9 [#migration-0-9] -### Set up the Supabase client [#migration-set-up-supabase-client] +#### Set up the Supabase client [#migration-set-up-supabase-client] In version 0.9 we now setup our Supabase client for the server inside of a `hooks.server.ts` file. @@ -398,7 +785,7 @@ In version 0.9 we now setup our Supabase client for the server inside of a `hook > -```js title=src/lib/db.ts +```js src/lib/db.ts // src/lib/db.ts import { createClient } from '@supabase/auth-helpers-sveltekit' import { env } from '$env/dynamic/public' @@ -412,7 +799,7 @@ export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_S -```js title=src/hooks.server.ts +```js src/hooks.server.ts // src/hooks.server.ts import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public' import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit' @@ -448,7 +835,7 @@ export const handle: Handle = async ({ event, resolve }) => { -### Initialize the client [#migration-initialize-client] +#### Initialize the client [#migration-initialize-client] In order to use the Supabase library in your client code you will need to setup a shared load function inside the root `+layout.ts` and create a `+layout.svelte` to handle our event listening for Auth events. @@ -460,7 +847,7 @@ In order to use the Supabase library in your client code you will need to setup > -```html title=src/routes/+layout.svelte +```svelte src/routes/+layout.svelte @@ -1319,7 +1706,7 @@ declare namespace App { -```html title=src/routes/+page.svelte +```svelte src/routes/+page.svelte @@ -1335,7 +1722,7 @@ declare namespace App { -### withPageAuth +#### withPageAuth -```html title=src/routes/protected-route.svelte +```svelte src/routes/protected-route.svelte + + + ``` + + + + + + + + + Start the app, navigate to http://localhost:3000 in the browser, open the browser console, and you should see the list of countries. @@ -122,26 +128,6 @@ The fastest way to get started with supabase and Nuxt.js is to use the supabase- - - - - - Next, in your Nuxt.js app, create a file called supabase-client.js and add the following code to initialize the Supabase client and set your project's credentials: - - - - - - ```js lib/supabaseClient.js - import { createClient } from '@supabase/supabase-js' - - const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key') - ``` - - - - - export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/getting-started/quickstarts/redwoodjs.mdx b/apps/docs/pages/guides/getting-started/quickstarts/redwoodjs.mdx index ac716078403..9398ef662c5 100644 --- a/apps/docs/pages/guides/getting-started/quickstarts/redwoodjs.mdx +++ b/apps/docs/pages/guides/getting-started/quickstarts/redwoodjs.mdx @@ -93,13 +93,12 @@ export const meta = { - ```bash terminal - # .env + ```bash .env # PostgreSQL connection string used for migrations DIRECT_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres" + # PostgreSQL connection string with pgBouncer config — used by Prisma Client DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true" - ``` @@ -113,9 +112,7 @@ export const meta = { - ```file="api/prisma/schema.prisma" title="api/prisma/schema.prisma" - // api/db/schema.prisma - + ```prisma api/prisma/schema.prisma datasource db { provider = "postgresql" url = env("DATABASE_URL") @@ -132,9 +129,7 @@ export const meta = { - ```js - // api/db/schema.prisma - + ```prisma api/db/schema.prisma model Country { id Int @id @default(autoincrement()) name String @unique @@ -153,9 +148,7 @@ export const meta = { - ```ts - // scripts/seed.ts - + ```ts scripts/seed.ts import type { Prisma } from '@prisma/client' import { db } from 'api/src/lib/db' diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-angular.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-angular.mdx index 819cf492c20..b46ffb7f967 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-angular.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-angular.mdx @@ -40,7 +40,7 @@ And finally we want to save the environment variables in the `environment.ts` fi All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys). These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. -```ts title=environment.ts +```ts environment.ts export const environment = { production: false, supabaseUrl: 'YOUR_SUPABASE_URL', @@ -50,7 +50,7 @@ export const environment = { Now that we have the API credentials in place, let's create a **SupabaseService** with `ng g s supabase` to initialize the Supabase client and implement functions to communicate with the Supabase API. -```ts title=src/app/supabase.service.ts +```ts src/app/supabase.service.ts import { Injectable } from '@angular/core' import { AuthChangeEvent, @@ -133,7 +133,7 @@ Optionally, update [src/styles.css](https://raw.githubusercontent.com/supabase/s Let's set up an Angular component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. Create an **AuthComponent** with `ng g c auth` Angular CLI command. -```ts title=src/app/auth/auth.component.ts +```ts src/app/auth/auth.component.ts import { Component } from '@angular/core' import { FormBuilder } from '@angular/forms' import { SupabaseService } from '../supabase.service' @@ -174,7 +174,7 @@ export class AuthComponent { } ``` -```html title=src/app/auth/auth.component.html +```html src/app/auth/auth.component.html

Supabase + Angular

@@ -205,7 +205,7 @@ export class AuthComponent { Users also need a way to edit their profile details and manage their accounts after signing in. Create an **AccountComponent** with the `ng g c account` Angular CLI command. -```ts title=src/app/account/account.component.ts +```ts src/app/account/account.component.ts import { Component, Input, OnInit } from '@angular/core' import { FormBuilder } from '@angular/forms' import { AuthSession } from '@supabase/supabase-js' @@ -295,7 +295,7 @@ export class AccountComponent implements OnInit { } ``` -```html title=src/app/account/account.component.html +```html src/app/account/account.component.html
@@ -326,7 +326,7 @@ export class AccountComponent implements OnInit { Now that we have all the components in place, let's update **AppComponent**: -```ts title=src/app/app.component.ts +```ts src/app/app.component.ts import { Component, OnInit } from '@angular/core' import { SupabaseService } from './supabase.service' @@ -348,7 +348,7 @@ export class AppComponent implements OnInit { } ``` -```html title=src/app/app.component.html +```html src/app/app.component.html
@@ -359,7 +359,7 @@ export class AppComponent implements OnInit { `app.module.ts` also needs to be modified to include the `ReactiveFormsModule` from the `@angular/forms` package. -```ts title=src/app/app.module.ts +```ts src/app/app.module.ts import { NgModule } from '@angular/core' import { BrowserModule } from '@angular/platform-browser' @@ -397,7 +397,7 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma Let's create an avatar for the user so that they can upload a profile photo. Create an **AvatarComponent** with `ng g c avatar` Angular CLI command. -```ts title=src/app/avatar/avatar.component.ts +```ts src/app/avatar/avatar.component.ts import { Component, EventEmitter, Input, Output } from '@angular/core' import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser' import { SupabaseService } from '../supabase.service' @@ -459,7 +459,7 @@ export class AvatarComponent { } ``` -```html title=src/app/avatar/avatar.component.html +```html src/app/avatar/avatar.component.html
@@ -498,7 +498,7 @@ And then we can add the widget on top of the **AccountComponent** html template: And add an `updateAvatar` function along with an `avatarUrl` getter to the **AccountComponent** typescript file: -```ts title=src/app/account.component.ts +```ts src/app/account.component.ts @Component({ selector: 'app-account', templateUrl: './account.component.html', diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-expo.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-expo.mdx index d4b0805ec49..7f2bcfaee5d 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-expo.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-expo.mdx @@ -44,7 +44,7 @@ We need the API URL and the `anon` key that you copied [earlier](#get-the-api-ke These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. -```ts title=lib/supabase.ts +```ts lib/supabase.ts import 'react-native-url-polyfill/auto' import * as SecureStore from 'expo-secure-store' import { createClient } from '@supabase/supabase-js' @@ -79,7 +79,7 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey, { Let's set up a React Native component to manage logins and sign ups. Users would be able to sign in with their email and password. -```tsx title=components/Auth.tsx +```tsx components/Auth.tsx import React, { useState } from 'react' import { Alert, StyleSheet, View } from 'react-native' import { supabase } from '../lib/supabase' @@ -167,7 +167,7 @@ After a user is signed in we can allow them to edit their profile details and ma Let's create a new component for that called `Account.tsx`. -```tsx title=components/Account.tsx +```tsx components/Account.tsx import { useState, useEffect } from 'react' import { supabase } from '../lib/supabase' import { StyleSheet, View, Alert } from 'react-native' @@ -294,7 +294,7 @@ const styles = StyleSheet.create({ Now that we have all the components in place, let's update `App.tsx`: -```tsx title=App.tsx +```tsx App.tsx import 'react-native-url-polyfill/auto' import { useState, useEffect } from 'react' import { supabase } from './lib/supabase' @@ -350,7 +350,7 @@ expo install react-native-document-picker Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component: -```tsx title=components/Avatar.tsx +```tsx components/Avatar.tsx import { useState, useEffect } from 'react' import { supabase } from '../lib/supabase' import { StyleSheet, View, Alert, Image, Button } from 'react-native' @@ -481,7 +481,7 @@ const styles = StyleSheet.create({ And then we can add the widget to the Account page: -```tsx title=components/Account.tsx +```tsx components/Account.tsx // Import the new component import Avatar from './Avatar' diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-flutter.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-flutter.mdx index 34a5e5c7aa0..70e6ef4c153 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-flutter.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-flutter.mdx @@ -3,6 +3,7 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { title: 'Build a User Management App with Flutter', description: 'Learn how to use Supabase in your Flutter App.', + tocVideo: 'r7ysVtZ5Row', } @@ -34,53 +35,37 @@ Then let's install the only additional dependency: [`supabase_flutter`](https:// Copy and paste the following line in your pubspec.yaml to install the package: ```yaml -supabase_flutter: ^1.0.0 +supabase_flutter: ^1.10.3 ``` Run `flutter pub get` to install the dependencies. ### Setup deep links -Now that we have the dependencies installed let's setup deep links so users who have logged in via magic link or OAuth can come back to the app. +Now that we have the dependencies installed let's setup deep links. +Setting up deep links is required to bring back the user to the app when they click on the magic link to sign in. +We can setup deep links with just a minor tweak on our Flutter application. -Add `io.supabase.flutterquickstart://login-callback/` as a new [redirect URL](https://app.supabase.com/project/_/auth/url-configuration) in the Dashboard. +We will use `io.supabase.flutterquickstart` as the scheme, and `login-callback` as the host for our deep link in this example, but you can change it to whatever you would like. + +First, add `io.supabase.flutterquickstart://login-callback/` as a new [redirect URL](https://app.supabase.com/project/_/auth/url-configuration) in the Dashboard. ![Supabase console deep link setting](/docs/img/deeplink-setting.png) That is it on Supabase's end and the rest are platform specific settings: -For Android, edit the `android/app/src/main/AndroidManifest.xml` file. - -Add an intent-filter to enable deep linking: - -```xml title=android/app/src/main/AndroidManifest.xml - - - - - - - - - - - - - - - - - - -``` - -For iOS, edit the ios/Runner/Info.plist file. + + +Edit the `ios/Runner/Info.plist` file. Add CFBundleURLTypes to enable deep linking: -```xml title=ios/Runner/Info.plist" +```xml ios/Runner/Info.plist" @@ -103,9 +88,40 @@ Add CFBundleURLTypes to enable deep linking: ``` -For web: + + +Edit the `android/app/src/main/AndroidManifest.xml` file. +Add an intent-filter to enable deep linking: + +```xml android/app/src/main/AndroidManifest.xml + + + + + + + + + + + + + + + + + + +``` + + + There are no additional configurations. + + ### Main function @@ -113,54 +129,29 @@ Now that we have deep links ready let's initialize the Supabase client inside ou These variables will be exposed on the app, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. -```dart title=lib/main.dart +```dart lib/main.dart Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - await Supabase.initialize( url: 'YOUR_SUPABASE_URL', anonKey: 'YOUR_SUPABASE_ANON_KEY', + authFlowType: AuthFlowType.pkce, ); runApp(MyApp()); } -``` - -### Setting up some constants and handy functions - -Let's also create a constant file to make it easier to use Supabase client. -We will also include an extension method declaration to call `showSnackBar` with one line of code. - -```dart title=lib/constants.dart -import 'package:flutter/material.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; final supabase = Supabase.instance.client; - -extension ShowSnackBar on BuildContext { - void showSnackBar({ - required String message, - Color backgroundColor = Colors.white, - }) { - ScaffoldMessenger.of(this).showSnackBar(SnackBar( - content: Text(message), - backgroundColor: backgroundColor, - )); - } - - void showErrorSnackBar({required String message}) { - showSnackBar(message: message, backgroundColor: Colors.red); - } -} ``` +`AuthFlowType.pkce` on `authFlowType` parameter indicates that we are using a secure [PKCE flow](https://supabase.com/blog/supabase-auth-sso-pkce#introducing-pkce) to perform our magic link login. + ### Set up Splash Screen Let's create a splash screen that will be shown to users right after they open the app. This screen retrieves the current session and redirects the user accordingly. -```dart title=lib/pages/splash_page.dart +```dart lib/pages/splash_page.dart import 'package:flutter/material.dart'; -import 'package:supabase_quickstart/constants.dart'; +import 'package:supabase_quickstart/main.dart'; class SplashPage extends StatefulWidget { const SplashPage({super.key}); @@ -170,20 +161,18 @@ class SplashPage extends StatefulWidget { } class _SplashPageState extends State { - bool _redirectCalled = false; @override - void didChangeDependencies() { - super.didChangeDependencies(); + void initState() { + super.initState(); _redirect(); } Future _redirect() async { await Future.delayed(Duration.zero); - if (_redirectCalled || !mounted) { + if (!mounted) { return; } - _redirectCalled = true; final session = supabase.auth.currentSession; if (session != null) { Navigator.of(context).pushReplacementNamed('/account'); @@ -199,7 +188,6 @@ class _SplashPageState extends State { ); } } - ``` ### Set up a Login page @@ -209,14 +197,13 @@ We'll use Magic Links, so users can sign in with their email without using passw Notice that this page sets up a listener on the user's auth state using `onAuthStateChange`. A new event will fire when the user comes back to the app by clicking their magic link, which this page can catch and redirect the user accordingly. -```dart title=lib/pages/login_page.dart +```dart lib/pages/login_page.dart import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; - -import 'package:supabase_quickstart/constants.dart'; +import 'package:supabase_quickstart/main.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -228,37 +215,46 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { bool _isLoading = false; bool _redirecting = false; - late final TextEditingController _emailController; + late final TextEditingController _emailController = TextEditingController(); late final StreamSubscription _authStateSubscription; Future _signIn() async { - setState(() { - _isLoading = true; - }); try { + setState(() { + _isLoading = true; + }); await supabase.auth.signInWithOtp( email: _emailController.text.trim(), emailRedirectTo: kIsWeb ? null : 'io.supabase.flutterquickstart://login-callback/', ); if (mounted) { - context.showSnackBar(message: 'Check your email for login link!'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Check your email for a login link!')), + ); _emailController.clear(); } } on AuthException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected error occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } } - - setState(() { - _isLoading = false; - }); } @override void initState() { - _emailController = TextEditingController(); _authStateSubscription = supabase.auth.onAuthStateChange.listen((data) { if (_redirecting) return; final session = data.session; @@ -307,11 +303,10 @@ class _LoginPageState extends State { After a user is signed in we can allow them to edit their profile details and manage their account. Let's create a new widget called `account_page.dart` for that. -```dart title=lib/pages/account_page.dart" +```dart lib/pages/account_page.dart" import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:supabase_quickstart/components/avatar.dart'; -import 'package:supabase_quickstart/constants.dart'; +import 'package:supabase_quickstart/main.dart'; class AccountPage extends StatefulWidget { const AccountPage({super.key}); @@ -323,8 +318,8 @@ class AccountPage extends StatefulWidget { class _AccountPageState extends State { final _usernameController = TextEditingController(); final _websiteController = TextEditingController(); - String? _avatarUrl; - var _loading = false; + + var _loading = true; /// Called once a user id is received within `onAuthenticated()` Future _getProfile() async { @@ -336,21 +331,28 @@ class _AccountPageState extends State { final userId = supabase.auth.currentUser!.id; final data = await supabase .from('profiles') - .select() + .select>() .eq('id', userId) - .single() as Map; + .single(); _usernameController.text = (data['username'] ?? '') as String; _websiteController.text = (data['website'] ?? '') as String; - _avatarUrl = (data['avatar_url'] ?? '') as String; } on PostgrestException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected exception occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + setState(() { + _loading = false; + }); + } } - - setState(() { - _loading = false; - }); } /// Called when user taps `Update` button @@ -370,28 +372,46 @@ class _AccountPageState extends State { try { await supabase.from('profiles').upsert(updates); if (mounted) { - context.showSnackBar(message: 'Successfully updated profile!'); + const SnackBar( + content: Text('Successfully updated profile!'), + ); } } on PostgrestException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpeted error occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + setState(() { + _loading = false; + }); + } } - setState(() { - _loading = false; - }); } Future _signOut() async { try { await supabase.auth.signOut(); } on AuthException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected error occurred'); - } - if (mounted) { - Navigator.of(context).pushReplacementNamed('/'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + Navigator.of(context).pushReplacementNamed('/login'); + } } } @@ -412,27 +432,29 @@ class _AccountPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Profile')), - body: ListView( - padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), - children: [ - TextFormField( - controller: _usernameController, - decoration: const InputDecoration(labelText: 'User Name'), - ), - const SizedBox(height: 18), - TextFormField( - controller: _websiteController, - decoration: const InputDecoration(labelText: 'Website'), - ), - const SizedBox(height: 18), - ElevatedButton( - onPressed: _updateProfile, - child: Text(_loading ? 'Saving...' : 'Update'), - ), - const SizedBox(height: 18), - TextButton(onPressed: _signOut, child: const Text('Sign Out')), - ], - ), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : ListView( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), + children: [ + TextFormField( + controller: _usernameController, + decoration: const InputDecoration(labelText: 'User Name'), + ), + const SizedBox(height: 18), + TextFormField( + controller: _websiteController, + decoration: const InputDecoration(labelText: 'Website'), + ), + const SizedBox(height: 18), + ElevatedButton( + onPressed: _loading ? null : _updateProfile, + child: Text(_loading ? 'Saving...' : 'Update'), + ), + const SizedBox(height: 18), + TextButton(onPressed: _signOut, child: const Text('Sign Out')), + ], + ), ); } } @@ -442,7 +464,7 @@ class _AccountPageState extends State { Now that we have all the components in place, let's update `lib/main.dart`: -```dart title=lib/main.dart +```dart lib/main.dart import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_quickstart/pages/account_page.dart'; @@ -451,13 +473,14 @@ import 'package:supabase_quickstart/pages/splash_page.dart'; Future main() async { await Supabase.initialize( - // TODO: Replace credentials with your own url: 'YOUR_SUPABASE_URL', anonKey: 'YOUR_SUPABASE_ANON_KEY', ); runApp(MyApp()); } +final supabase = Supabase.instance.client; + class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { @@ -535,11 +558,11 @@ Once you are done with all of the above, it is time to dive into coding. Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component: -```dart title=lib/components/avatar.dart +```dart lib/components/avatar.dart import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:supabase_quickstart/constants.dart'; +import 'package:supabase_quickstart/main.dart'; class Avatar extends StatefulWidget { const Avatar({ @@ -614,11 +637,21 @@ class _AvatarState extends State { widget.onUpload(imageUrlResponse); } on StorageException catch (error) { if (mounted) { - context.showErrorSnackBar(message: error.message); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); } } catch (error) { if (mounted) { - context.showErrorSnackBar(message: 'Unexpected error occurred'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); } } @@ -631,11 +664,11 @@ class _AvatarState extends State { And then we can add the widget to the Account page as well as some logic to update the `avatar_url` whenever the user uploads a new avatar. -```dart title=lib/pages/account_page.dart +```dart lib/pages/account_page.dart import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_quickstart/components/avatar.dart'; -import 'package:supabase_quickstart/constants.dart'; +import 'package:supabase_quickstart/main.dart'; class AccountPage extends StatefulWidget { const AccountPage({super.key}); @@ -647,8 +680,9 @@ class AccountPage extends StatefulWidget { class _AccountPageState extends State { final _usernameController = TextEditingController(); final _websiteController = TextEditingController(); + String? _avatarUrl; - var _loading = false; + var _loading = true; /// Called once a user id is received within `onAuthenticated()` Future _getProfile() async { @@ -660,21 +694,29 @@ class _AccountPageState extends State { final userId = supabase.auth.currentUser!.id; final data = await supabase .from('profiles') - .select() + .select>() .eq('id', userId) - .single() as Map; + .single(); _usernameController.text = (data['username'] ?? '') as String; _websiteController.text = (data['website'] ?? '') as String; _avatarUrl = (data['avatar_url'] ?? '') as String; } on PostgrestException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected exception occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + setState(() { + _loading = false; + }); + } } - - setState(() { - _loading = false; - }); } /// Called when user taps `Update` button @@ -694,28 +736,46 @@ class _AccountPageState extends State { try { await supabase.from('profiles').upsert(updates); if (mounted) { - context.showSnackBar(message: 'Successfully updated profile!'); + const SnackBar( + content: Text('Successfully updated profile!'), + ); } } on PostgrestException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpeted error occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + setState(() { + _loading = false; + }); + } } - setState(() { - _loading = false; - }); } Future _signOut() async { try { await supabase.auth.signOut(); } on AuthException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected error occurred'); - } - if (mounted) { - Navigator.of(context).pushReplacementNamed('/'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); + } finally { + if (mounted) { + Navigator.of(context).pushReplacementNamed('/login'); + } } } @@ -728,12 +788,20 @@ class _AccountPageState extends State { 'avatar_url': imageUrl, }); if (mounted) { - context.showSnackBar(message: 'Updated your profile image!'); + const SnackBar( + content: Text('Updated your profile image!'), + ); } } on PostgrestException catch (error) { - context.showErrorSnackBar(message: error.message); + SnackBar( + content: Text(error.message), + backgroundColor: Theme.of(context).colorScheme.error, + ); } catch (error) { - context.showErrorSnackBar(message: 'Unexpected error has occurred'); + SnackBar( + content: const Text('Unexpected error occurred'), + backgroundColor: Theme.of(context).colorScheme.error, + ); } if (!mounted) { return; @@ -761,32 +829,34 @@ class _AccountPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Profile')), - body: ListView( - padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), - children: [ - Avatar( - imageUrl: _avatarUrl, - onUpload: _onUpload, - ), - const SizedBox(height: 18), - TextFormField( - controller: _usernameController, - decoration: const InputDecoration(labelText: 'User Name'), - ), - const SizedBox(height: 18), - TextFormField( - controller: _websiteController, - decoration: const InputDecoration(labelText: 'Website'), - ), - const SizedBox(height: 18), - ElevatedButton( - onPressed: _updateProfile, - child: Text(_loading ? 'Saving...' : 'Update'), - ), - const SizedBox(height: 18), - TextButton(onPressed: _signOut, child: const Text('Sign Out')), - ], - ), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : ListView( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), + children: [ + Avatar( + imageUrl: _avatarUrl, + onUpload: _onUpload, + ), + const SizedBox(height: 18), + TextFormField( + controller: _usernameController, + decoration: const InputDecoration(labelText: 'User Name'), + ), + const SizedBox(height: 18), + TextFormField( + controller: _websiteController, + decoration: const InputDecoration(labelText: 'Website'), + ), + const SizedBox(height: 18), + ElevatedButton( + onPressed: _loading ? null : _updateProfile, + child: Text(_loading ? 'Saving...' : 'Update'), + ), + const SizedBox(height: 18), + TextButton(onPressed: _signOut, child: const Text('Sign Out')), + ], + ), ); } } diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-angular.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-angular.mdx index 43516ef65f3..f381c1ec529 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-angular.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-angular.mdx @@ -41,7 +41,7 @@ And finally we want to save the environment variables in the `environment.ts` fi All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys). These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. -```ts title=environment.ts +```ts environment.ts export const environment = { production: false, supabaseUrl: 'YOUR_SUPABASE_URL', @@ -51,7 +51,7 @@ export const environment = { Now that we have the API credentials in place, let's create a **SupabaseService** with `ionic g s supabase` to initialize the Supabase client and implement functions to communicate with the Supabase API. -```ts title=src/app/supabase.service.ts +```ts src/app/supabase.service.ts import { Injectable } from '@angular/core' import { LoadingController, ToastController } from '@ionic/angular' import { AuthChangeEvent, createClient, Session, SupabaseClient } from '@supabase/supabase-js' @@ -139,7 +139,7 @@ Create an **LoginPage** with `ionic g page login` Ionic CLI command. > This guide will show the template inline, but the example app will have templateUrls -```ts title=src/app/login/login.page.ts +```ts src/app/login/login.page.ts import { Component, OnInit } from '@angular/core' import { SupabaseService } from '../supabase.service' @@ -198,7 +198,7 @@ export class LoginPage implements OnInit { After a user is signed in we can allow them to edit their profile details and manage their account. Create an **AccountComponent** with `ionic g page account` Ionic CLI command. -```ts title=src/app/account.component.ts +```ts src/app/account.component.ts import { Component, OnInit } from '@angular/core' import { Router } from '@angular/router' import { Profile, SupabaseService } from '../supabase.service' @@ -292,7 +292,7 @@ export class AccountPage implements OnInit { Now that we have all the components in place, let's update **AppComponent**: -```ts title=src/app/app.component.ts +```ts src/app/app.component.ts import { Component } from '@angular/core' import { Router } from '@angular/router' import { SupabaseService } from './supabase.service' @@ -320,7 +320,7 @@ export class AppComponent { Then update the **AppRoutingModule** -```ts title=src/app/app.ts" +```ts src/app/app.ts" import { NgModule } from '@angular/core' import { PreloadAllModules, RouterModule, Routes } from '@angular/router' @@ -376,7 +376,7 @@ Ionic PWA elements is a companion package that will polyfill certain browser API With those packages installed we can update our `main.ts` to include an additional bootstapping call for the Ionic PWA Elements. -```ts title=src/main.ts +```ts src/main.ts import { enableProdMode } from '@angular/core' import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' @@ -400,7 +400,7 @@ Then create an **AvatarComponent** with this Ionic CLI command: ionic g component avatar --module=/src/app/account/account.module.ts --create-module ``` -```ts title=src/app/avatar.component.ts +```ts src/app/avatar.component.ts import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' import { SupabaseService } from '../supabase.service' @@ -503,7 +503,7 @@ export class AvatarComponent implements OnInit { And then we can add the widget on top of the **AccountComponent** html template: -```ts title=src/app/account.component.ts +```ts src/app/account.component.ts template: ` diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-react.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-react.mdx index 3e3d261a5e4..fbb17ca5464 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-react.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-react.mdx @@ -40,7 +40,7 @@ npm install @supabase/supabase-js And finally we want to save the environment variables in a `.env`. All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys). -```bash title=.env +```bash .env REACT_APP_SUPABASE_URL=YOUR_SUPABASE_URL REACT_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY ``` @@ -48,7 +48,7 @@ REACT_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client. These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. -```js title=src/supabaseClient.js +```js src/supabaseClient.js import { createClient } from '@supabase/supabase-js' const supabaseUrl = process.env.REACT_APP_SUPABASE_URL @@ -61,7 +61,7 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey) Let's set up a React component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. -```jsx title=/src/pages/Login.tsx +```jsx /src/pages/Login.tsx import { useState } from 'react'; import { IonButton, @@ -140,7 +140,7 @@ After a user is signed in we can allow them to edit their profile details and ma Let's create a new component for that called `Account.tsx`. -```jsx title=src/pages/Account.tsx +```jsx src/pages/Account.tsx import { IonButton, IonContent, @@ -294,7 +294,7 @@ export function AccountPage() { Now that we have all the components in place, let's update `App.tsx`: -```jsx title=src/App.tsx +```jsx src/App.tsx import { Redirect, Route } from 'react-router-dom' import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react' import { IonReactRouter } from '@ionic/react-router' @@ -370,7 +370,7 @@ Ionic PWA elements is a companion package that will polyfill certain browser API With those packages installed we can update our `index.tsx` to include an additional bootstapping call for the Ionic PWA Elements. -```ts title=src/index.tsx +```ts src/index.tsx import React from 'react' import ReactDOM from 'react-dom' import App from './App' @@ -393,7 +393,7 @@ reportWebVitals() Then create an **AvatarComponent**. -```jsx title=src/components/Avatar.tsx +```jsx src/components/Avatar.tsx import { IonIcon } from '@ionic/react'; import { person } from 'ionicons/icons'; import { Camera, CameraResultType } from '@capacitor/camera'; @@ -476,7 +476,7 @@ export function Avatar({ And then we can add the widget to the Account page: -```jsx title=src/pages/Account.tsx +```jsx src/pages/Account.tsx // Import the new component import { Avatar } from '../components/Avatar'; diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-vue.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-vue.mdx index dc884f9d8d5..b58717103ae 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-vue.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-vue.mdx @@ -40,7 +40,7 @@ npm install @supabase/supabase-js And finally we want to save the environment variables in a `.env`. All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys). -```bash title=.env +```bash .env VUE_APP_SUPABASE_URL=YOUR_SUPABASE_URL VUE_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY ``` @@ -48,7 +48,7 @@ VUE_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client. These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. -```js title=src/supabase.ts" +```js src/supabase.ts" import { createClient } from '@supabase/supabase-js'; const supabaseUrl = process.env.VUE_APP_SUPABASE_URL as string; @@ -61,7 +61,7 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey); Let's set up a Vue component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. -```html title=/src/views/Login.vue +```html /src/views/Login.vue