mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Merge branch 'master' into fix/realtime-docs
This commit is contained in:
commit
47209f6ad3
969 files changed
+126466
-109150
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
node-version: [18.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
@@ -34,5 +34,8 @@ jobs:
|
||||
run: npm ci
|
||||
working-directory: ./
|
||||
- name: Run tests
|
||||
env:
|
||||
# Default is 2 GB, increase to have less frequent OOM errors
|
||||
NODE_OPTIONS: '--max_old_space_size=3072'
|
||||
run: npm run test:studio
|
||||
working-directory: ./
|
||||
@@ -0,0 +1,30 @@
|
||||
name: UI Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
|
||||
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:ui
|
||||
@@ -122,5 +122,8 @@ 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
|
||||
@@ -1 +1 @@
|
||||
^./i18n
|
||||
^./i18n
|
||||
+8
-9
@@ -17,14 +17,13 @@
|
||||
|
||||
## 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)
|
||||
@@ -48,7 +47,7 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
|
||||
git clone https://github.com/<github_username>/supabase.git
|
||||
```
|
||||
|
||||
1. Go to the Supabase directory:
|
||||
2. Go to the Supabase directory:
|
||||
```sh
|
||||
cd supabase
|
||||
```
|
||||
@@ -63,7 +62,7 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
|
||||
npm install # install dependencies
|
||||
```
|
||||
|
||||
2. You can then run the apps simultaneously with the following.
|
||||
2. After that you can run the apps simultaneously with the following.
|
||||
```sh
|
||||
npm run dev # start all the applications
|
||||
```
|
||||
@@ -121,7 +120,7 @@ Now when you run a local development docs server you will see the new docs site.
|
||||
|
||||
After making your changes, open a pull request (PR). Once you submit your pull request, others from the Supabase team/community will review it with you.
|
||||
|
||||
Did you have an issue, like a merge conflict, or don't know how to open a pull request? Check out [GitHub's pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) tutorial on how to resolve merge conflicts and other issues. Once your PR has been merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/supabase/supabase/graphs/contributors).
|
||||
If you have an issue, like a merge conflict, or don't know how to open a pull request then check out [GitHub's pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) tutorial on how to resolve merge conflicts and other issues. Once your PR has been merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/supabase/supabase/graphs/contributors).
|
||||
|
||||
---
|
||||
|
||||
@@ -135,7 +134,7 @@ Create a new entry in the [`redirects.js`](https://github.com/supabase/supabase/
|
||||
|
||||
## Community channels
|
||||
|
||||
Stuck somewhere? Have any questions? Join the [Discord Community Server](https://discord.supabase.com/) or the [Github Discussions](https://github.com/supabase/supabase/discussions). We are here to help!
|
||||
If you are stuck somewhere or have any questions, join our [Discord Community Server](https://discord.supabase.com/) or the [Github Discussions](https://github.com/supabase/supabase/discussions). We are here to help!
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
@@ -79,6 +80,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<th>Client</th>
|
||||
<th colspan="5">Feature-Clients (bundled in Supabase client)</th>
|
||||
</tr>
|
||||
<!-- notranslate -->
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Supabase</th>
|
||||
@@ -99,7 +101,9 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td><a href="https://github.com/supabase-community/storage-lang" target="_blank" rel="noopener noreferrer">storage-lang</a></td>
|
||||
</tr>
|
||||
END ROW -->
|
||||
<!-- /notranslate -->
|
||||
<th colspan="7">⚡️ Official ⚡️</th>
|
||||
<!-- notranslate -->
|
||||
<tr>
|
||||
<td>JavaScript (TypeScript)</td>
|
||||
<td><a href="https://github.com/supabase/supabase-js" target="_blank" rel="noopener noreferrer">supabase-js</a></td>
|
||||
@@ -118,7 +122,9 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
|
||||
</tr>
|
||||
<!-- /notranslate -->
|
||||
<th colspan="7">💚 Community 💚</th>
|
||||
<!-- notranslate -->
|
||||
<tr>
|
||||
<td>C#</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-csharp" target="_blank" rel="noopener noreferrer">supabase-csharp</a></td>
|
||||
@@ -200,6 +206,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td><a href="https://github.com/supabase-community/storage-gdscript" target="_blank" rel="noopener noreferrer">storage-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-gdscript" target="_blank" rel="noopener noreferrer">functions-gdscript</a></td>
|
||||
</tr>
|
||||
<!-- /notranslate -->
|
||||
</table>
|
||||
|
||||
<!--- Remove this list if you're translating to another language, it's hard to keep updated across multiple files-->
|
||||
@@ -212,23 +219,28 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
- [Bangla / বাংলা](/i18n/README.bn.md)
|
||||
- [Bulgarian / Български](/i18n/README.bg.md)
|
||||
- [Catalan / Català](/i18n/README.ca.md)
|
||||
- [Czech / čeština](/i18n/README.cs.md)
|
||||
- [Danish / Dansk](/i18n/README.da.md)
|
||||
- [Dutch / Nederlands](/i18n/README.nl.md)
|
||||
- [English](https://github.com/supabase/supabase)
|
||||
- [Estonian / eesti keel](/i18n/README.et.md)
|
||||
- [Finnish / Suomalainen](/i18n/README.fi.md)
|
||||
- [French / Français](/i18n/README.fr.md)
|
||||
- [German / Deutsch](/i18n/README.de.md)
|
||||
- [Greek / Ελληνικά](/i18n/README.gr.md)
|
||||
- [Greek / Ελληνικά](/i18n/README.el.md)
|
||||
- [Gujarati / ગુજરાતી](/i18n/README.gu.md)
|
||||
- [Hebrew / עברית](/i18n/README.he.md)
|
||||
- [Hindi / हिंदी](/i18n/README.hi.md)
|
||||
- [Hungarian / Magyar](/i18n/README.hu.md)
|
||||
- [Nepali / नेपाली](/i18n/README.ne.md)
|
||||
- [Indonesian / Bahasa Indonesia](/i18n/README.id.md)
|
||||
- [Italian / Italiano](/i18n/README.it.md)
|
||||
- [Japanese / 日本語](/i18n/README.jp.md)
|
||||
- [Italiano / Italian](/i18n/README.it.md)
|
||||
- [Japanese / 日本語](/i18n/README.ja.md)
|
||||
- [Korean / 한국어](/i18n/README.ko.md)
|
||||
- [Lithuanian / lietuvių](/i18n/README.lt.md)
|
||||
- [Latvian / latviski](/i18n/README.lv.md)
|
||||
- [Malay / Bahasa Malaysia](/i18n/README.ms.md)
|
||||
- [Norwegian (Bokmål) / Norsk (Bokmål)](/i18n/README.nb-no.md)
|
||||
- [Norwegian (Bokmål) / Norsk (Bokmål)](/i18n/README.nb.md)
|
||||
- [Persian / فارسی](/i18n/README.fa.md)
|
||||
- [Polish / Polski](/i18n/README.pl.md)
|
||||
- [Portuguese / Português](/i18n/README.pt.md)
|
||||
@@ -237,6 +249,8 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
- [Russian / Pусский](/i18n/README.ru.md)
|
||||
- [Serbian / Srpski](/i18n/README.sr.md)
|
||||
- [Sinhala / සිංහල](/i18n/README.si.md)
|
||||
- [Slovak / slovenský](/i18n/README.sk.md)
|
||||
- [Slovenian / Slovenščina](/i18n/README.sl.md)
|
||||
- [Spanish / Español](/i18n/README.es.md)
|
||||
- [Simplified Chinese / 简体中文](/i18n/README.zh-cn.md)
|
||||
- [Swedish / Svenska](/i18n/README.sv.md)
|
||||
@@ -247,8 +261,3 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
- [Vietnamese / Tiếng Việt](/i18n/README.vi-vn.md)
|
||||
- [List of translations](/i18n/languages.md) <!--- Keep only this -->
|
||||
|
||||
---
|
||||
|
||||
## Sponsors
|
||||
|
||||
[](https://github.com/sponsors/supabase)
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import { FC } from 'react'
|
||||
import { IconInfo, IconHelpCircle, IconAlertTriangle } from 'ui'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { IconAlertTriangle, IconHelpCircle, IconInfo } from 'ui'
|
||||
|
||||
interface Props {
|
||||
export interface AdmonitionProps {
|
||||
type: 'note' | 'tip' | 'info' | 'caution' | 'danger'
|
||||
label?: string
|
||||
children: any
|
||||
}
|
||||
|
||||
const Admonition: FC<Props> = ({ type = 'note', label, children }) => {
|
||||
const Admonition = ({ type = 'note', label, children }: PropsWithChildren<AdmonitionProps>) => {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { useState } from 'react'
|
||||
import { Input, Button } from 'ui'
|
||||
import Admonition from '~/components/Admonition'
|
||||
|
||||
function base64URL(value: string) {
|
||||
return globalThis.btoa(value).replace(/[=]/g, '').replace(/[+]/g, '-').replace(/[\/]/g, '_')
|
||||
}
|
||||
|
||||
/*
|
||||
Convert a string into an ArrayBuffer
|
||||
from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
||||
*/
|
||||
function stringToArrayBuffer(value: string) {
|
||||
const buf = new ArrayBuffer(value.length)
|
||||
const bufView = new Uint8Array(buf)
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
bufView[i] = value.charCodeAt(i)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
function arrayBufferToString(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint8Array(buf))
|
||||
}
|
||||
|
||||
const generateAppleSecretKey = async (
|
||||
kid: string,
|
||||
iss: string,
|
||||
sub: string,
|
||||
file: File
|
||||
): Promise<{ kid: string; jwt: string; exp: number }> => {
|
||||
if (!kid) {
|
||||
const match = file.name.match(/AuthKey_([^.]+)[.].*$/i)
|
||||
if (match && match[1]) {
|
||||
kid = match[1]
|
||||
}
|
||||
}
|
||||
|
||||
if (!kid) {
|
||||
throw new Error(
|
||||
`No Key ID provided. The file "${file.name}" does not follow the AuthKey_XXXXXXXXXX.p8 pattern. Please provide a Key ID manually.`
|
||||
)
|
||||
}
|
||||
|
||||
const contents = await file.text()
|
||||
|
||||
if (!contents.match(/^\s*-+BEGIN PRIVATE KEY-+[^-]+-+END PRIVATE KEY-+\s*$/i)) {
|
||||
throw new Error(`Chosen file does not appear to be a PEM encoded PKCS8 private key file.`)
|
||||
}
|
||||
|
||||
// remove PEM headers and spaces
|
||||
const pkcs8 = stringToArrayBuffer(
|
||||
globalThis.atob(contents.replace(/-+[^-]+-+/g, '').replace(/\s+/g, ''))
|
||||
)
|
||||
|
||||
const privateKey = await globalThis.crypto.subtle.importKey(
|
||||
'pkcs8',
|
||||
pkcs8,
|
||||
{
|
||||
name: 'ECDSA',
|
||||
namedCurve: 'P-256',
|
||||
},
|
||||
true,
|
||||
['sign']
|
||||
)
|
||||
|
||||
const iat = Math.floor(Date.now() / 1000)
|
||||
const exp = iat + 180 * 24 * 60 * 60
|
||||
|
||||
const jwt = [
|
||||
base64URL(JSON.stringify({ typ: 'JWT', kid, alg: 'ES256' })),
|
||||
base64URL(
|
||||
JSON.stringify({
|
||||
iss,
|
||||
sub,
|
||||
iat,
|
||||
exp,
|
||||
aud: 'https://appleid.apple.com',
|
||||
})
|
||||
),
|
||||
]
|
||||
|
||||
const signature = await globalThis.crypto.subtle.sign(
|
||||
{
|
||||
name: 'ECDSA',
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
privateKey,
|
||||
stringToArrayBuffer(jwt.join('.'))
|
||||
)
|
||||
|
||||
jwt.push(base64URL(arrayBufferToString(signature)))
|
||||
|
||||
return { kid, jwt: jwt.join('.'), exp }
|
||||
}
|
||||
|
||||
const AppleSecretGenerator = () => {
|
||||
const [file, setFile] = useState({ file: null as File | null })
|
||||
const [teamID, setTeamID] = useState('')
|
||||
const [serviceID, setServiceID] = useState('')
|
||||
const [keyID, setKeyID] = useState('')
|
||||
const [secretKey, setSecretKey] = useState('')
|
||||
const [expiresAt, setExpiresAt] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
label="Account ID"
|
||||
labelOptional="required"
|
||||
placeholder="Apple Developer account ID, 10 alphanumeric digits"
|
||||
descriptionText="Found in the upper-right corner of Apple Developer Center."
|
||||
value={teamID}
|
||||
onChange={(e) => setTeamID(e.target.value.trim())}
|
||||
/>
|
||||
<Input
|
||||
label="Service ID"
|
||||
labelOptional="required"
|
||||
placeholder="ID of the service, example: com.example.app.service"
|
||||
descriptionText="Found under Certificates, Identifiers & Profiles in Apple Developer Center."
|
||||
value={serviceID}
|
||||
onChange={(e) => setServiceID(e.target.value.trim())}
|
||||
/>
|
||||
<Input
|
||||
label="Key ID"
|
||||
labelOptional="(optional)"
|
||||
placeholder="Extracted from filename, AuthKey_XXXXXXXXXX.p8"
|
||||
descriptionText="If the file you select does not preserve the original name from Apple Developer Center, please enter the key ID."
|
||||
value={keyID}
|
||||
onChange={(e) => setKeyID(e.target.value.trim())}
|
||||
/>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
setFile({ file: e.target.files[0] })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ height: '1rem' }} />
|
||||
|
||||
<Button
|
||||
size="medium"
|
||||
disabled={
|
||||
!(
|
||||
teamID.length === 10 &&
|
||||
serviceID &&
|
||||
((globalThis && globalThis.showOpenFilePicker) || file.file)
|
||||
)
|
||||
}
|
||||
onClick={async () => {
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const { kid, jwt, exp } = await generateAppleSecretKey(
|
||||
keyID,
|
||||
teamID,
|
||||
serviceID,
|
||||
file.file
|
||||
)
|
||||
setKeyID(kid)
|
||||
setSecretKey(jwt)
|
||||
setExpiresAt(new Date(exp * 1000).toString())
|
||||
setError('')
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
console.error(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Generate Secret Key
|
||||
</Button>
|
||||
|
||||
{error && <Admonition type="danger">{error}</Admonition>}
|
||||
|
||||
{secretKey && (
|
||||
<>
|
||||
<div style={{ height: '1rem' }} />
|
||||
<Input
|
||||
label="Secret Key"
|
||||
value={secretKey}
|
||||
descriptionText={`Valid until: ${expiresAt}. Make sure you generate a new one before then!`}
|
||||
reveal
|
||||
copy
|
||||
size="medium"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppleSecretGenerator
|
||||
@@ -0,0 +1,96 @@
|
||||
import { getAnchor, removeAnchor } from './CustomHTMLElements.utils'
|
||||
|
||||
describe('CustomHTMLElementsUtils', () => {
|
||||
describe('getAnchor', () => {
|
||||
describe('when value is an object', () => {
|
||||
it('returns slugified version of props.children', () => {
|
||||
const value = {
|
||||
props: {
|
||||
children: 'Full Text Search',
|
||||
},
|
||||
}
|
||||
|
||||
const result = getAnchor(value)
|
||||
|
||||
expect(result).toStrictEqual('full-text-search')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when value is an array', () => {
|
||||
describe('when custom anchor exists', () => {
|
||||
it('returns inner slug', () => {
|
||||
const value = ['Proximity: <->', '[#proximity]']
|
||||
|
||||
const result = getAnchor(value)
|
||||
|
||||
expect(result).toStrictEqual('proximity')
|
||||
})
|
||||
|
||||
it('trims whitespace', () => {
|
||||
const value = ['Proximity: <->', ' [#proximity] ']
|
||||
|
||||
const result = getAnchor(value)
|
||||
|
||||
expect(result).toStrictEqual('proximity')
|
||||
})
|
||||
})
|
||||
|
||||
it('returns concatenated slug of elements', () => {
|
||||
const value = ['Full', 'Text', 'Search']
|
||||
|
||||
const result = getAnchor(value)
|
||||
|
||||
expect(result).toStrictEqual('full-text-search')
|
||||
})
|
||||
|
||||
it('trims whitespace', () => {
|
||||
const value = [' Full ', ' Text ', ' Search ']
|
||||
|
||||
const result = getAnchor(value)
|
||||
|
||||
expect(result).toStrictEqual('full-text-search')
|
||||
})
|
||||
|
||||
it('removes special characters', () => {
|
||||
const value = ['function()']
|
||||
|
||||
const result = getAnchor(value)
|
||||
|
||||
expect(result).toStrictEqual('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when value is a string', () => {
|
||||
it('returns slugified version of string', () => {
|
||||
const value = 'My (Very) Awesome Heading'
|
||||
|
||||
const result = getAnchor(value)
|
||||
|
||||
expect(result).toStrictEqual('my-very-awesome-heading')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeAnchor', () => {
|
||||
describe('when value is an array', () => {
|
||||
it('filters out custom anchor elements', () => {
|
||||
const value = ['My (Very) Awesome Heading', '[#my-custom-heading]']
|
||||
|
||||
const result = removeAnchor(value)
|
||||
|
||||
expect(result).toStrictEqual(['My (Very) Awesome Heading'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when value is a string', () => {
|
||||
it('strips out custom anchor string', () => {
|
||||
const value = 'My (Very) Awesome Heading [#my-custom-heading]'
|
||||
|
||||
const result = removeAnchor(value)
|
||||
|
||||
// Original implementation didn't trim the resulting string - not sure if it really matters
|
||||
expect(result).toStrictEqual('My (Very) Awesome Heading ')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,53 +2,60 @@
|
||||
export const getAnchor = (text: any): string | undefined => {
|
||||
if (typeof text === 'object') {
|
||||
if (Array.isArray(text)) {
|
||||
const customAnchor = text.find(
|
||||
(x) => typeof x === 'string' && x.includes('[#') && x.endsWith(']')
|
||||
)
|
||||
if (customAnchor !== undefined) return customAnchor.slice(2, customAnchor.indexOf(']'))
|
||||
const customAnchor = text.find((x) => typeof x === 'string' && hasCustomAnchor(x))
|
||||
if (customAnchor !== undefined) {
|
||||
return parseCustomAnchor(customAnchor)
|
||||
}
|
||||
|
||||
const formattedText = text
|
||||
.map((x) => {
|
||||
if (typeof x !== 'string') return x.props.children
|
||||
else return x.trim()
|
||||
if (typeof x !== 'string') {
|
||||
return x.props.children
|
||||
}
|
||||
|
||||
return x.trim()
|
||||
})
|
||||
.map((x) => {
|
||||
if (typeof x !== 'string') return x
|
||||
else
|
||||
if (typeof x !== 'string') {
|
||||
return x
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9- ]/g, '')
|
||||
.replace(/[ ]/g, '-')
|
||||
}
|
||||
|
||||
return slugify(x)
|
||||
})
|
||||
|
||||
return formattedText.join('-').toLowerCase()
|
||||
} else {
|
||||
const anchor = text.props.children
|
||||
if (typeof anchor === 'string') {
|
||||
return anchor
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9- ]/g, '')
|
||||
.replace(/[ ]/g, '-')
|
||||
return slugify(anchor)
|
||||
}
|
||||
return anchor
|
||||
}
|
||||
} else if (typeof text === 'string') {
|
||||
if (text.includes('[#') && text.endsWith(']')) {
|
||||
return text.slice(text.indexOf('[#') + 2, text.indexOf(']'))
|
||||
} else {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9- ]/g, '')
|
||||
.replace(/[ ]/g, '-')
|
||||
if (hasCustomAnchor(text)) {
|
||||
return parseCustomAnchor(text)
|
||||
}
|
||||
return slugify(text)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const hasCustomAnchor = (value: string): boolean => value.includes('[#') && value.includes(']')
|
||||
|
||||
const parseCustomAnchor = (value: string): string =>
|
||||
value.slice(value.indexOf('[#') + 2, value.indexOf(']'))
|
||||
|
||||
const slugify = (value: string): string =>
|
||||
value
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9- ]/g, '')
|
||||
.replace(/[ ]/g, '-')
|
||||
|
||||
export const removeAnchor = (text: any) => {
|
||||
if (typeof text === 'object' && Array.isArray(text)) {
|
||||
return text.filter((x) => !(typeof x === 'string' && x.includes('[#') && x.endsWith(']')))
|
||||
return text.filter((x) => !(typeof x === 'string' && hasCustomAnchor(x)))
|
||||
} else if (typeof text === 'string') {
|
||||
if (text.indexOf('[#') > 0) return text.slice(0, text.indexOf('[#'))
|
||||
else return text
|
||||
|
||||
@@ -37,8 +37,8 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M124.951 5.03906V224.602"
|
||||
stroke="url(#paint0_radial_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -46,10 +46,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M125.25 191.383C167.828 191.383 202.344 156.867 202.344 114.289C202.344 71.7115 167.828 37.1953 125.25 37.1953C82.6724 37.1953 48.1562 71.7115 48.1562 114.289C48.1562 156.867 82.6724 191.383 125.25 191.383Z"
|
||||
stroke="var(--colors-scale11)"
|
||||
stroke-opacity="0.1"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.1"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -57,10 +57,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M33.7782 58.8377C48.6666 58.8377 60.7361 46.7682 60.7361 31.8798C60.7361 16.9913 48.6666 4.92188 33.7782 4.92188C18.8898 4.92188 6.82031 16.9913 6.82031 31.8798C6.82031 46.7682 18.8898 58.8377 33.7782 58.8377Z"
|
||||
stroke="url(#paint1_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -68,10 +68,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M33.7782 223.664C48.6666 223.664 60.7361 211.594 60.7361 196.706C60.7361 181.817 48.6666 169.748 33.7782 169.748C18.8898 169.748 6.82031 181.817 6.82031 196.706C6.82031 211.594 18.8898 223.664 33.7782 223.664Z"
|
||||
stroke="url(#paint2_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -79,10 +79,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M216.724 58.8377C231.612 58.8377 243.681 46.7682 243.681 31.8798C243.681 16.9913 231.612 4.92188 216.724 4.92188C201.835 4.92188 189.766 16.9913 189.766 31.8798C189.766 46.7682 201.835 58.8377 216.724 58.8377Z"
|
||||
stroke="url(#paint3_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -90,36 +90,36 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M216.724 223.664C231.612 223.664 243.681 211.594 243.681 196.706C243.681 181.817 231.612 169.748 216.724 169.748C201.835 169.748 189.766 181.817 189.766 196.706C189.766 211.594 201.835 223.664 216.724 223.664Z"
|
||||
stroke="url(#paint4_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
animate={pathMotionConfig.animate}
|
||||
transition={pathMotionConfig.transition as any}
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M209.29 226.982H40.6907C18.7665 226.982 1 209.51 1 187.949V40.0333C1 18.4722 18.7665 1 40.6907 1H209.29C231.214 1 248.981 18.4722 248.981 40.0333V187.969C248.981 209.51 231.214 226.982 209.29 226.982Z"
|
||||
stroke="url(#paint5_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
animate={pathMotionConfig.animate}
|
||||
transition={pathMotionConfig.transition as any}
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M216.724 223.903H33.7779C18.719 223.903 6.5 211.684 6.5 196.625V31.9576C6.5 16.8987 18.719 4.67969 33.7779 4.67969H216.724C231.782 4.67969 244.001 16.8987 244.001 31.9576V196.625C244.001 211.704 231.802 223.903 216.724 223.903Z"
|
||||
stroke="url(#paint6_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -127,10 +127,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M60.7578 30.9199V197.027"
|
||||
stroke="url(#paint7_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -138,10 +138,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M125.252 5.01953V223.562"
|
||||
stroke="url(#paint8_radial_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -149,10 +149,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M149.809 4.83984L13.5996 178.866"
|
||||
stroke="url(#paint9_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -160,10 +160,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M239.679 46.457L99.5703 223.643"
|
||||
stroke="url(#paint10_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -171,10 +171,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M189.766 30.9199V197.027"
|
||||
stroke="url(#paint11_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -182,9 +182,9 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M218.024 58.7344H32.498"
|
||||
stroke="url(#paint12_radial_0_1)"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -192,10 +192,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M6.76172 95.4141H243.743"
|
||||
stroke="url(#paint13_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -203,10 +203,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M6.76172 131.869H243.743"
|
||||
stroke="url(#paint14_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -214,10 +214,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M218.024 169.846H32.498"
|
||||
stroke="var(--colors-scale11)"
|
||||
stroke-opacity="0.1"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.1"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -225,10 +225,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M126.559 4.6867L126.559 224.254"
|
||||
stroke="url(#paint15_linear_0_1)"
|
||||
stroke-opacity="0.5"
|
||||
stroke-width="0.7"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
strokeOpacity="0.5"
|
||||
strokeWidth="0.7"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
<g filter="url(#filter0_d_0_1)">
|
||||
<m.path
|
||||
@@ -237,8 +237,8 @@ const DocsCoverLogo = () => {
|
||||
transition={logoMotionConfig.transition as any}
|
||||
d="M125.751 174.003C125.833 179.464 134.091 181.807 137.49 177.53L188.786 112.985C194.84 105.367 189.41 94.1317 179.674 94.1317H126.964L127.046 52.5389C126.964 47.078 119.027 44.7344 115.628 49.0116L65.5801 113.556C59.5255 121.175 64.9558 132.41 74.6925 132.41H125.161L125.751 174.003Z"
|
||||
fill="url(#paint16_linear_0_1)"
|
||||
fill-opacity="0.79"
|
||||
shape-rendering="crispEdges"
|
||||
fillOpacity="0.79"
|
||||
shapeRendering="crispEdges"
|
||||
/>
|
||||
<m.path
|
||||
initial={pathMotionConfig.initial}
|
||||
@@ -246,10 +246,10 @@ const DocsCoverLogo = () => {
|
||||
transition={pathMotionConfig.transition as any}
|
||||
d="M126.964 94.6797L126.416 94.1306L126.498 52.5425C126.459 50.117 124.68 48.3327 122.422 47.6478C120.162 46.9621 117.586 47.432 116.059 49.3499C116.059 49.3508 116.058 49.3516 116.057 49.3525L66.0131 113.892L66.0091 113.897C60.2405 121.156 65.4135 131.862 74.6925 131.862H125.161L125.709 132.402L126.299 173.995L126.299 173.995M126.964 94.6797L125.751 174.003L126.299 173.995M126.964 94.6797H179.674C188.953 94.6797 194.126 105.386 188.357 112.644L137.061 177.189C135.539 179.104 132.883 179.583 130.531 178.892C128.18 178.202 126.336 176.41 126.299 173.995M126.964 94.6797L126.299 173.995"
|
||||
stroke="url(#paint17_linear_0_1)"
|
||||
stroke-width="1.09591"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
shape-rendering="crispEdges"
|
||||
strokeWidth="1.09591"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
shapeRendering="crispEdges"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
@@ -260,9 +260,9 @@ const DocsCoverLogo = () => {
|
||||
width="137.062"
|
||||
height="141.703"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
@@ -289,8 +289,8 @@ const DocsCoverLogo = () => {
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(125.451 114.82) rotate(90) scale(109.781 0.5)"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_0_1"
|
||||
@@ -300,8 +300,8 @@ const DocsCoverLogo = () => {
|
||||
y2="11.9994"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_0_1"
|
||||
@@ -311,8 +311,8 @@ const DocsCoverLogo = () => {
|
||||
y2="204.984"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_0_1"
|
||||
@@ -322,8 +322,8 @@ const DocsCoverLogo = () => {
|
||||
y2="13.9993"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_0_1"
|
||||
@@ -333,8 +333,8 @@ const DocsCoverLogo = () => {
|
||||
y2="204.984"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint5_linear_0_1"
|
||||
@@ -344,9 +344,9 @@ const DocsCoverLogo = () => {
|
||||
y2="-7.94661"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop offset="0.489583" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
<stop offset="0.489583" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint6_linear_0_1"
|
||||
@@ -356,9 +356,9 @@ const DocsCoverLogo = () => {
|
||||
y2="-3.99932"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop offset="0.489583" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
<stop offset="0.489583" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint7_linear_0_1"
|
||||
@@ -368,9 +368,9 @@ const DocsCoverLogo = () => {
|
||||
y2="199.061"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale10)" stop-opacity="0.1" />
|
||||
<stop offset="0.505208" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0.1" />
|
||||
<stop stopColor="var(--colors-scale10)" stopOpacity="0.1" />
|
||||
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.1" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="paint8_radial_0_1"
|
||||
@@ -380,8 +380,8 @@ const DocsCoverLogo = () => {
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(125.752 114.291) rotate(90) scale(109.271 0.5)"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint9_linear_0_1"
|
||||
@@ -391,8 +391,8 @@ const DocsCoverLogo = () => {
|
||||
y2="6.31757"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale11)" stop-opacity="0.3" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop stopColor="var(--colors-scale11)" stopOpacity="0.3" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint10_linear_0_1"
|
||||
@@ -402,8 +402,8 @@ const DocsCoverLogo = () => {
|
||||
y2="200"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale11)" stop-opacity="0.3" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop stopColor="var(--colors-scale11)" stopOpacity="0.3" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint11_linear_0_1"
|
||||
@@ -413,9 +413,9 @@ const DocsCoverLogo = () => {
|
||||
y2="199.061"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale10)" stop-opacity="0.1" />
|
||||
<stop offset="0.505208" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0.1" />
|
||||
<stop stopColor="var(--colors-scale10)" stopOpacity="0.1" />
|
||||
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.1" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="paint12_radial_0_1"
|
||||
@@ -425,8 +425,8 @@ const DocsCoverLogo = () => {
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(125.261 59.2344) rotate(90) scale(0.5 263.749)"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0" />
|
||||
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint13_linear_0_1"
|
||||
@@ -436,9 +436,9 @@ const DocsCoverLogo = () => {
|
||||
y2="94.9749"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale10)" stop-opacity="0.5" />
|
||||
<stop offset="0.505208" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0.5" />
|
||||
<stop stopColor="var(--colors-scale10)" stopOpacity="0.5" />
|
||||
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint14_linear_0_1"
|
||||
@@ -448,9 +448,9 @@ const DocsCoverLogo = () => {
|
||||
y2="131.43"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale10)" stop-opacity="0.5" />
|
||||
<stop offset="0.505208" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0.5" />
|
||||
<stop stopColor="var(--colors-scale10)" stopOpacity="0.5" />
|
||||
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint15_linear_0_1"
|
||||
@@ -460,9 +460,9 @@ const DocsCoverLogo = () => {
|
||||
y2="1.99785"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--colors-scale10)" stop-opacity="0.1" />
|
||||
<stop offset="0.505208" stop-color="var(--colors-scale11)" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="var(--colors-scale10)" stop-opacity="0.1" />
|
||||
<stop stopColor="var(--colors-scale10)" stopOpacity="0.1" />
|
||||
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
|
||||
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint16_linear_0_1"
|
||||
@@ -472,8 +472,8 @@ const DocsCoverLogo = () => {
|
||||
y2="206.049"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#212121" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.12" />
|
||||
<stop stopColor="#212121" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.12" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint17_linear_0_1"
|
||||
@@ -483,9 +483,9 @@ const DocsCoverLogo = () => {
|
||||
y2="175.889"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#868585" />
|
||||
<stop offset="0.208333" stop-color="#838383" />
|
||||
<stop offset="1" stop-color="#5A5A5A" />
|
||||
<stop stopColor="#868585" />
|
||||
<stop offset="0.208333" stopColor="#838383" />
|
||||
<stop offset="1" stopColor="#5A5A5A" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</m.svg>
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -17,7 +17,7 @@ const HomePageCover = (props) => {
|
||||
p-5 md:p-8
|
||||
"
|
||||
>
|
||||
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-1 md:gap-6">
|
||||
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-1 md:gap-3">
|
||||
<div className="md:max-w-xs xl:max-w-none">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<IconBackground>
|
||||
@@ -29,7 +29,7 @@ const HomePageCover = (props) => {
|
||||
Discover how to set up a database to an app making queries in just a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap md:grid md:grid-cols-4 gap-2 sm:gap-3">
|
||||
<div className="flex flex-wrap md:grid md:grid-cols-4 2xl:grid-cols-7 gap-2 sm:gap-3">
|
||||
<Link href={`/guides/getting-started/quickstarts/reactjs`} passHref>
|
||||
<a className="no-underline">
|
||||
<IconPanel
|
||||
@@ -118,7 +118,7 @@ const HomePageCover = (props) => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full xl:max-w-[365px] -mb-40">
|
||||
<div className="w-full xl:max-w-[365px] 2xl:max-w-[608px] -mb-40">
|
||||
<GettingStarted />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
## Deployment
|
||||
|
||||
If you have your own infrastructure for deploying Python apps, you can continue to use `vecs` as described in this guide.
|
||||
|
||||
Alternatively if you would like to quickly deploy using Supabase, check out our guide on using the [Hugging Face Inference API](/docs/guides/ai/hugging-face) in Edge Functions using TypeScript.
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Tabs } from 'ui'
|
||||
export const TabPanel = Tabs.Panel
|
||||
|
||||
## Project setup
|
||||
|
||||
Let's create a new Postgres database. This is as simple as starting a new Project in Supabase:
|
||||
|
||||
1. [Create a new project](https://database.new/) in the Supabase dashboard.
|
||||
1. Enter your project details. Remember to store your password somewhere safe.
|
||||
|
||||
Your database will be available in less than a minute.
|
||||
|
||||
**Finding your credentials:**
|
||||
|
||||
You can find your project credentials inside the project [settings](https://app.supabase.com/project/_/settings/), including:
|
||||
|
||||
- [Database credentials](https://app.supabase.com/project/_/settings/database): connection strings and connection pooler details.
|
||||
- [API credentials](https://app.supabase.com/project/_/settings/database): your serverless API URL and `anon` / `service_role` keys.
|
||||
@@ -11,7 +11,7 @@ Enable the [http extension for the `extensions` schema](https://app.supabase.com
|
||||
Then, define the following SQL functions in the SQL Editor to delete
|
||||
storage objects via the API:
|
||||
|
||||
```SQL
|
||||
```sql
|
||||
create or replace function delete_storage_object(bucket text, object text, out status int, out content text)
|
||||
returns record
|
||||
language 'plpgsql'
|
||||
@@ -52,7 +52,7 @@ $$;
|
||||
Next, add a trigger that removes any obsolete avatar whenever the
|
||||
profile is updated or deleted:
|
||||
|
||||
```SQL
|
||||
```sql
|
||||
create or replace function delete_old_avatar()
|
||||
returns trigger
|
||||
language 'plpgsql'
|
||||
@@ -89,7 +89,7 @@ Finally, delete the `public.profile` row before a user is deleted.
|
||||
If this step is omitted, you won't be able to delete users without
|
||||
first manually deleting their avatar image.
|
||||
|
||||
```SQL
|
||||
```sql
|
||||
create or replace function delete_old_profile()
|
||||
returns trigger
|
||||
language 'plpgsql'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '~/../../packages/ui'
|
||||
import { Button } from 'ui'
|
||||
import { primaryLinks, secondaryLinks } from '~/data/footer'
|
||||
import { LayoutMainContent } from '../../layouts/DefaultLayout'
|
||||
|
||||
@@ -21,8 +21,8 @@ const Footer = () => (
|
||||
d={icon}
|
||||
stroke="var(--colors-scale9)"
|
||||
strokeWidth={0.7}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
@@ -71,8 +71,8 @@ const Footer = () => (
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.66832 1.55469C4.98649 1.55469 2.00195 4.54402 2.00195 8.23269C2.00195 11.1827 3.91187 13.686 6.56108 14.5687C6.8944 14.63 7.01573 14.424 7.01573 14.2467C7.01573 14.0887 7.0104 13.668 7.00706 13.1114C5.15248 13.5147 4.76116 12.216 4.76116 12.216C4.45851 11.444 4.0212 11.2387 4.0212 11.2387C3.41589 10.8254 4.06719 10.8334 4.06719 10.8334C4.73583 10.88 5.08782 11.5214 5.08782 11.5214C5.68246 12.5414 6.64841 12.2467 7.02773 12.076C7.08906 11.6447 7.26105 11.3507 7.45171 11.184C5.97178 11.0154 4.41518 10.442 4.41518 7.88335C4.41518 7.15469 4.67517 6.55802 5.10115 6.09135C5.03248 5.92269 4.80383 5.24335 5.16648 4.32469C5.16648 4.32469 5.72645 4.14469 6.99973 5.00869C7.54355 4.86036 8.10464 4.78482 8.66832 4.78402C9.23496 4.78669 9.80494 4.86069 10.3376 5.00869C11.6102 4.14469 12.1688 4.32402 12.1688 4.32402C12.5328 5.24335 12.3035 5.92269 12.2355 6.09135C12.6621 6.55802 12.9208 7.15469 12.9208 7.88335C12.9208 10.4487 11.3615 11.0134 9.87694 11.1787C10.1163 11.3847 10.3289 11.792 10.3289 12.4154C10.3289 13.3074 10.3209 14.028 10.3209 14.2467C10.3209 14.4254 10.4409 14.6334 10.7796 14.568C12.107 14.1228 13.261 13.2716 14.0784 12.1347C14.8958 10.9979 15.3353 9.6329 15.3347 8.23269C15.3347 4.54402 12.3495 1.55469 8.66832 1.55469Z"
|
||||
fill="var(--colors-scale9)"
|
||||
/>
|
||||
@@ -135,10 +135,10 @@ const Footer = () => (
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_1182_99731)">
|
||||
<g clipPath="url(#clip0_1182_99731)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.8781 3.13594C12.8583 2.66802 11.7648 2.32328 10.6215 2.12583C10.6006 2.12202 10.5798 2.13154 10.5691 2.15059C10.4285 2.40073 10.2727 2.72706 10.1636 2.98356C8.93387 2.79945 7.71044 2.79945 6.50592 2.98356C6.39681 2.72136 6.23538 2.40073 6.09411 2.15059C6.08339 2.13218 6.06259 2.12265 6.04176 2.12583C4.89905 2.32265 3.80554 2.66739 2.78517 3.13594C2.77633 3.13975 2.76876 3.14611 2.76374 3.15435C0.689563 6.25326 0.12136 9.276 0.400102 12.2613C0.401363 12.2759 0.409561 12.2898 0.420913 12.2987C1.78939 13.3037 3.115 13.9139 4.41599 14.3183C4.43681 14.3247 4.45887 14.317 4.47212 14.2999C4.77987 13.8796 5.0542 13.4364 5.28941 12.9704C5.3033 12.9431 5.29005 12.9107 5.26167 12.9C4.82654 12.7349 4.4122 12.5336 4.01364 12.3051C3.98212 12.2867 3.97959 12.2416 4.00859 12.22C4.09247 12.1571 4.17636 12.0917 4.25645 12.0257C4.27094 12.0136 4.29113 12.0111 4.30816 12.0187C6.92655 13.2142 9.76126 13.2142 12.3488 12.0187C12.3658 12.0105 12.386 12.013 12.4011 12.0251C12.4812 12.0911 12.5651 12.1571 12.6496 12.22C12.6786 12.2416 12.6767 12.2867 12.6452 12.3051C12.2466 12.5381 11.8323 12.7349 11.3965 12.8993C11.3681 12.9101 11.3555 12.9431 11.3694 12.9704C11.6097 13.4358 11.884 13.879 12.1861 14.2993C12.1987 14.317 12.2214 14.3247 12.2422 14.3183C13.5495 13.9139 14.8751 13.3037 16.2436 12.2987C16.2556 12.2898 16.2631 12.2765 16.2644 12.2619C16.598 8.8106 15.7056 5.81265 13.8989 3.15498C13.8944 3.14611 13.8869 3.13975 13.8781 3.13594ZM5.68043 10.4435C4.89211 10.4435 4.24257 9.71978 4.24257 8.83093C4.24257 7.94207 4.87952 7.21831 5.68043 7.21831C6.48763 7.21831 7.13089 7.94843 7.11827 8.83093C7.11827 9.71978 6.48132 10.4435 5.68043 10.4435ZM10.9967 10.4435C10.2084 10.4435 9.55884 9.71978 9.55884 8.83093C9.55884 7.94207 10.1958 7.21831 10.9967 7.21831C11.8039 7.21831 12.4471 7.94843 12.4345 8.83093C12.4345 9.71978 11.8039 10.4435 10.9967 10.4435Z"
|
||||
fill="var(--colors-scale9)"
|
||||
/>
|
||||
|
||||
@@ -9,8 +9,8 @@ export interface NavMenuGroup {
|
||||
|
||||
export interface NavMenuSection {
|
||||
name: string
|
||||
url?: string
|
||||
items: NavMenuSection[]
|
||||
url?: `/${string}`
|
||||
items: Partial<NavMenuSection>[]
|
||||
}
|
||||
|
||||
export interface References {
|
||||
@@ -26,10 +26,17 @@ export interface References {
|
||||
type MenuItem = {
|
||||
label: string
|
||||
icon?: string
|
||||
href?: string
|
||||
href?: `/${string}` | `https://${string}`
|
||||
level?: string
|
||||
hasLightIcon?: boolean
|
||||
community?: boolean
|
||||
}
|
||||
|
||||
export type HomepageMenuItems = MenuItem[][]
|
||||
|
||||
export type NavMenuConstant = Readonly<{
|
||||
title: string
|
||||
icon: string
|
||||
url?: `/${string}`
|
||||
items: ReadonlyArray<Partial<NavMenuSection>>
|
||||
}>
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
IconMenuSwift,
|
||||
IconMenuStatus,
|
||||
IconMenuKotlin,
|
||||
IconMenuAI,
|
||||
} from './HomeMenuIcons'
|
||||
|
||||
function getMenuIcon(menuKey: string, width: number = 16, height: number = 16) {
|
||||
@@ -41,6 +42,8 @@ function getMenuIcon(menuKey: string, width: number = 16, height: number = 16) {
|
||||
return <IconMenuRealtime width={width} height={height} />
|
||||
case 'storage':
|
||||
return <IconMenuStorage width={width} height={height} />
|
||||
case 'ai':
|
||||
return <IconMenuAI width={width} height={height} />
|
||||
case 'platform':
|
||||
return <IconMenuPlatform width={width} height={height} />
|
||||
case 'resources':
|
||||
|
||||
@@ -356,8 +356,28 @@ export function IconMenuStorage({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
<path
|
||||
d="M13 7.616V5.6l-3.618-3.6H3v4.03m9.964-.447L9.38 2v3.584h3.584ZM1.974 6v8h12V7.509h-7.59l-1.533-1.51H1.974Z"
|
||||
stroke="currentColor"
|
||||
stroke-miterlimit="10"
|
||||
stroke-linejoin="bevel"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuAI({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.99886 7.63216V14.4892M7.99886 7.63216L14.0488 4.11804M7.99886 7.63216L1.94922 4.11819M1.94922 4.11819V8.32332M1.94922 4.11819V4.08217L5.57319 1.97717M14.049 8.36007V4.08217L10.4251 1.97717M11.8165 12.4072L7.99913 14.6245L4.18177 12.4072"
|
||||
stroke="currentColor"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -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: [
|
||||
@@ -280,15 +286,6 @@ export const gettingstarted = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'AI & ML',
|
||||
items: [
|
||||
{
|
||||
name: 'Vector Search with OpenAI',
|
||||
url: '/guides/getting-started/openai/vector-search',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -321,7 +318,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 +472,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 +504,42 @@ 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: '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 +646,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 +663,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 +704,7 @@ export const api = {
|
||||
],
|
||||
}
|
||||
|
||||
export const functions = {
|
||||
export const functions: NavMenuConstant = {
|
||||
icon: 'edge-functions',
|
||||
title: 'Edge Functions',
|
||||
url: '/guides/functions',
|
||||
@@ -728,7 +732,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' },
|
||||
{
|
||||
@@ -749,7 +753,8 @@ 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: 'Upstash Redis', url: '/guides/functions/examples/upstash-redis' },
|
||||
{ name: 'Type-Safe SQL with Kysely', url: '/guides/functions/kysely-postgres' },
|
||||
],
|
||||
@@ -758,7 +763,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 +783,7 @@ export const functions = {
|
||||
],
|
||||
}
|
||||
|
||||
export const realtime = {
|
||||
export const realtime: NavMenuConstant = {
|
||||
icon: 'realtime',
|
||||
title: 'Realtime',
|
||||
url: '/guides/realtime',
|
||||
@@ -841,7 +846,7 @@ export const realtime = {
|
||||
],
|
||||
}
|
||||
|
||||
export const storage = {
|
||||
export const storage: NavMenuConstant = {
|
||||
icon: 'storage',
|
||||
title: 'Storage',
|
||||
url: '/guides/storage',
|
||||
@@ -855,7 +860,103 @@ 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: '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 +978,7 @@ export const supabase_cli = {
|
||||
],
|
||||
}
|
||||
|
||||
export const platform = {
|
||||
export const platform: NavMenuConstant = {
|
||||
icon: 'platform',
|
||||
title: 'Platform',
|
||||
url: '/guides/platform',
|
||||
@@ -932,9 +1033,19 @@ export const platform = {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Go-live Checklist',
|
||||
name: 'Production Readiness',
|
||||
url: undefined,
|
||||
items: [{ name: 'Production Readiness', url: '/guides/platform/going-into-prod' }],
|
||||
items: [
|
||||
{
|
||||
name: 'Shared Responsibility Model',
|
||||
url: '/guides/platform/shared-responsibility-model',
|
||||
},
|
||||
{
|
||||
name: 'Maturity Model',
|
||||
url: '/guides/platform/maturity-model',
|
||||
},
|
||||
{ name: 'Production Checklist', url: '/guides/platform/going-into-prod' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Troubleshooting',
|
||||
@@ -953,7 +1064,7 @@ export const platform = {
|
||||
],
|
||||
}
|
||||
|
||||
export const resources = {
|
||||
export const resources: NavMenuConstant = {
|
||||
icon: 'resources',
|
||||
title: 'Resources',
|
||||
url: '/guides/resources',
|
||||
@@ -994,7 +1105,7 @@ export const resources = {
|
||||
],
|
||||
}
|
||||
|
||||
export const self_hosting = {
|
||||
export const self_hosting: NavMenuConstant = {
|
||||
title: 'Self-Hosting',
|
||||
icon: 'self-hosting',
|
||||
url: '/guides/self-hosting',
|
||||
@@ -1051,7 +1162,7 @@ export const migrate = {
|
||||
],
|
||||
}
|
||||
|
||||
export const integrations = {
|
||||
export const integrations: NavMenuConstant = {
|
||||
icon: 'integrations',
|
||||
title: 'Integrations',
|
||||
url: '/guides/integrations',
|
||||
@@ -1082,8 +1193,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' },
|
||||
@@ -1103,6 +1215,7 @@ export const integrations = {
|
||||
{ name: 'Directus', url: '/guides/integrations/directus' },
|
||||
{ name: 'Draftbit', url: '/guides/integrations/draftbit' },
|
||||
{ name: 'FlutterFlow', url: '/guides/integrations/flutterflow' },
|
||||
{ name: 'Forest Admin', url: '/guides/integrations/forestadmin' },
|
||||
{ name: 'Plasmic', url: '/guides/integrations/plasmic' },
|
||||
{ name: 'ILLA', url: '/guides/integrations/illa' },
|
||||
],
|
||||
|
||||
@@ -68,6 +68,11 @@ const menus: Menu[] = [
|
||||
path: '/guides/storage',
|
||||
type: 'guide',
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
path: '/guides/ai',
|
||||
type: 'guide',
|
||||
},
|
||||
{
|
||||
id: 'platform',
|
||||
path: '/guides/platform',
|
||||
|
||||
@@ -46,7 +46,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
|
||||
if (activeItem && activeItemRef.current) {
|
||||
// this is a hack, but seems a common one on Stackoverflow
|
||||
setTimeout(() => {
|
||||
activeItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
activeItemRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
}, 0)
|
||||
}
|
||||
})
|
||||
@@ -147,7 +147,7 @@ const Content = (props) => {
|
||||
const { menu, id } = props
|
||||
|
||||
return (
|
||||
<ul className={['relative w-full flex flex-col gap-0'].join(' ')}>
|
||||
<ul className={['relative w-full flex flex-col gap-0 pb-5'].join(' ')}>
|
||||
<Link href={`${menu.parent ?? '/'}`} passHref>
|
||||
<a
|
||||
className={[
|
||||
|
||||
@@ -197,7 +197,7 @@ const NavigationMenuRefListItems = ({
|
||||
<HeaderLink title={menu.title} url={menu.url} id={id} />
|
||||
<RevVersionDropdown />
|
||||
</div>
|
||||
<ul className="function-link-list flex flex-col gap-2">
|
||||
<ul className="function-link-list flex flex-col gap-2 pb-5">
|
||||
{filteredSections.map((section) => {
|
||||
return (
|
||||
<Fragment key={section.title}>
|
||||
|
||||
@@ -129,7 +129,7 @@ const TopNavBar: FC = () => {
|
||||
type="outline"
|
||||
as="a"
|
||||
// @ts-ignore
|
||||
href="https://app.supabase.com"
|
||||
href="https://supabase.com/dashboard"
|
||||
className="text-scale-1100 text-sm"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
|
||||
@@ -119,7 +119,7 @@ const TopNavBarRef: FC = () => {
|
||||
type="text"
|
||||
as="a"
|
||||
// @ts-ignore
|
||||
href="https://app.supabase.com"
|
||||
href="https://supabase.com/dashboard"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
|
||||
@@ -16,12 +16,13 @@ import FunctionsExamples from './FunctionsExamples'
|
||||
import { Mermaid } from 'mdx-mermaid/lib/Mermaid'
|
||||
import RefSubLayout from '~/layouts/ref/RefSubLayout'
|
||||
import { Heading } from './CustomHTMLElements'
|
||||
import DatabaseSetup from './MDX/database_setup.mdx'
|
||||
import ProjectSetup from './MDX/project_setup.mdx'
|
||||
import QuickstartIntro from './MDX/quickstart_intro.mdx'
|
||||
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
|
||||
import SocialProviderSetup from './MDX/social_provider_setup.mdx'
|
||||
import StorageManagement from './MDX/storage_management.mdx'
|
||||
// import { CH } from '@code-hike/mdx/components'
|
||||
import { CH } from '@code-hike/mdx/components'
|
||||
import RefHeaderSection from './reference/RefHeaderSection'
|
||||
|
||||
// Ref version specific
|
||||
@@ -58,6 +59,7 @@ const components = {
|
||||
Admonition,
|
||||
Button,
|
||||
ButtonCard,
|
||||
CH,
|
||||
CodeBlock,
|
||||
GlassPanel,
|
||||
Link,
|
||||
@@ -66,6 +68,7 @@ const components = {
|
||||
FunctionsExamples,
|
||||
JwtGenerator,
|
||||
QuickstartIntro,
|
||||
DatabaseSetup,
|
||||
ProjectSetup,
|
||||
SocialProviderSetup,
|
||||
SocialProviderSettingsSupabase,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { extractTsDocNode, generateParameters } from '~/lib/refGenerator/helpers
|
||||
import RefDetailCollapse from '~/components/reference/RefDetailCollapse'
|
||||
import { Fragment } from 'react'
|
||||
import { IRefFunctionSection } from './Reference.types'
|
||||
import components from '~/components'
|
||||
|
||||
const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
|
||||
const item = props.spec.functions.find((x: any) => x.id === props.funcData.id)
|
||||
@@ -38,16 +39,16 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
|
||||
<header className={['prose'].join(' ')}>
|
||||
{shortText && <ReactMarkdown className="text-sm">{shortText}</ReactMarkdown>}
|
||||
</header>
|
||||
|
||||
{item.description && (
|
||||
<div className="prose">
|
||||
<ReactMarkdown className="text-sm">{item.description}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.notes && (
|
||||
<div className="prose">
|
||||
<ReactMarkdown className="text-sm">{item.notes}</ReactMarkdown>
|
||||
<ReactMarkdown className="text-sm" components={components}>
|
||||
{item.notes}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
{/* // parameters */}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,6 +3,174 @@ id: release-notes
|
||||
title: Release Notes
|
||||
---
|
||||
|
||||
## 0.11.0 - 2023-05-24
|
||||
|
||||
- Update dependency: postgrest-csharp@3.2.0
|
||||
- General codebase and QOL improvements. Exceptions are generally thrown through `PostgrestException` now instead
|
||||
of `Exception`. A `FailureHint.Reason` is provided with failures if possible to parse.
|
||||
- `AddDebugListener` is now available on the client to help with debugging
|
||||
- Merges [#65](https://github.com/supabase-community/postgrest-csharp/pull/65) Cleanup + Add better exception handling
|
||||
- Merges [#66](https://github.com/supabase-community/postgrest-csharp/pull/66) Local test Fixes
|
||||
- Fixes [#67](https://github.com/supabase-community/postgrest-csharp/issues/67) Postgrest Reference attribute is
|
||||
producing StackOverflow for circular references
|
||||
- Update dependency: gotrue-csharp@4.0.2
|
||||
- [#58](https://github.com/supabase-community/gotrue-csharp/issues/58) - Add support for the `reauthentication` endpoint which allows for secure password changes.
|
||||
- Update dependency: realtime-csharp@6.0.1
|
||||
- Updates publishing action for future packages, includes README and icon.
|
||||
- Merges [#28](https://github.com/supabase-community/realtime-csharp/pull/28) and [#30](https://github.com/supabase-community/realtime-csharp/pull/30)
|
||||
- The realtime client now takes a "fail-fast" approach. On establishing an initial connection, client will throw
|
||||
a `RealtimeException` in `ConnectAsync()` if the socket server is unreachable. After an initial connection has been
|
||||
established, the **client will continue attempting reconnections indefinitely until disconnected.**
|
||||
- [Major, New] C# `EventHandlers` have been changed to `delegates`. This should allow for cleaner event data access over
|
||||
the previous subclassed `EventArgs` setup. Events are scoped accordingly. For example, the `RealtimeSocket` error
|
||||
handlers will receive events regarding socket connectivity; whereas the `RealtimeChannel` error handlers will receive
|
||||
events according to `Channel` joining/leaving/etc. This is implemented with the following methods prefixed by (
|
||||
Add/Remove/Clear):
|
||||
- `RealtimeBroadcast.AddBroadcastEventHandler`
|
||||
- `RealtimePresence.AddPresenceEventHandler`
|
||||
- `RealtimeSocket.AddStateChangedHandler`
|
||||
- `RealtimeSocket.AddMessageReceivedHandler`
|
||||
- `RealtimeSocket.AddHeartbeatHandler`
|
||||
- `RealtimeSocket.AddErrorHandler`
|
||||
- `RealtimeClient.AddDebugHandler`
|
||||
- `RealtimeClient.AddStateChangedHandler`
|
||||
- `RealtimeChannel.AddPostgresChangeHandler`
|
||||
- `RealtimeChannel.AddMessageReceivedHandler`
|
||||
- `RealtimeChannel.AddErrorHandler`
|
||||
- `Push.AddMessageReceivedHandler`
|
||||
- [Major, new] `ClientOptions.Logger` has been removed in favor of `Client.AddDebugHandler()` which allows for
|
||||
implementing custom logging solutions if desired.
|
||||
- A simple logger can be set up with the following:
|
||||
```c#
|
||||
client.AddDebugHandler((sender, message, exception) => Debug.WriteLine(message));
|
||||
```
|
||||
- [Major] `Connect()` has been marked `Obsolete` in favor of `ConnectAsync()`
|
||||
- Custom reconnection logic has been removed in favor of using the built-in logic from `Websocket.Client@4.6.1`.
|
||||
- Exceptions that are handled within this library have been marked as `RealtimeException`s.
|
||||
- The local, docker-composed test suite has been brought back (as opposed to remotely testing on live supabase servers)
|
||||
to test against.
|
||||
- Comments have been added throughout the entire codebase and an `XML` file is now generated on build.
|
||||
|
||||
## 0.10.0 - 2023-05-14
|
||||
|
||||
- Changes options to require `Supabase.SupabaseOptions.SessionPersistor` from using `ISupabaseSessionHandler`
|
||||
to `IGotrueSessionPersistance<Session>` (these are now synchronous operations).
|
||||
- Update dependency: gotrue-csharp@4.0.1
|
||||
- [#60](https://github.com/supabase-community/gotrue-csharp/pull/60) - Add interfaces, bug fixes, additional error
|
||||
reason detection. Thanks [@wiverson](https://github.com/wiverson)!
|
||||
- [#57](https://github.com/supabase-community/gotrue-csharp/pull/57) Refactor exceptions, code cleanup, and move to
|
||||
delegate auth state changes
|
||||
- Huge thank you to [@wiverson](https://github.com/wiverson) for his help on this refactor and release!
|
||||
- Changes
|
||||
- Exceptions have been simplified to a single `GotrueException`. A `Reason` field has been added
|
||||
to `GotrueException` to clarify what happened. This should also be easier to manage as the Gotrue
|
||||
server API & messages evolve.
|
||||
- The session delegates for `Save`/`Load`/`Destroy` have been simplified to no longer require `async`.
|
||||
- Console logging in a few places (most notable the background refresh thread) has been removed
|
||||
in favor of a notification method. See `Client.AddDebugListener()` and the test cases for examples.
|
||||
This will allow you to implement your own logging strategy (write to temp file, console, user visible
|
||||
err console, etc).
|
||||
- The client now more reliably emits AuthState changes.
|
||||
- There is now a single source of truth for headers in the stateful Client - the `Options` headers.
|
||||
- New feature:
|
||||
- Added a `Settings` request to the stateless API only - you can now query the server instance to
|
||||
determine if it's got the settings you need. This might allow for things like a visual
|
||||
component in a tool to verify the GoTrue settings are working correctly, or tests that run differently
|
||||
depending on the server configuration.
|
||||
- Implementation notes:
|
||||
- Test cases have been added to help ensure reliability of auth state change notifications
|
||||
and persistence.
|
||||
- Persistence is now managed via the same notifications as auth state change
|
||||
|
||||
## 0.9.1 - 2023-04-28
|
||||
|
||||
- Update dependency: gotrue-csharp@3.1.1
|
||||
- Implements `SignInWithIdToken` for Apple/Google signing from LW7. A HUGE thank you
|
||||
to [@wiverson](https://github.com/wiverson)!
|
||||
- Update dependency: realtime-csharp@5.0.5
|
||||
- Re: [#27](https://github.com/supabase-community/realtime-csharp/issues/27) `PostgresChangesOptions` was not
|
||||
setting `listenType` in constructor. Thanks [@Kuffs2205](https://github.com/Kuffs2205)
|
||||
- Update dependency: supabase-storage-csharp@1.2.10
|
||||
- Re: [#7](https://github.com/supabase-community/storage-csharp/issues/7) Implements a `DownloadPublicFile` method.
|
||||
|
||||
## 0.9.0 - 2023-04-12
|
||||
|
||||
- Update dependency: gotrue-csharp@3.1.0
|
||||
|
||||
- [Minor] Implements PKCE auth flow. SignIn using a provider now returns an instance of `ProviderAuthState` rather
|
||||
than a `string`.
|
||||
|
||||
- Update dependency: supabase-storage-csharp@1.2.9
|
||||
- Implements storage features from LW7:
|
||||
- feat: custom file size limit and mime types at bucket
|
||||
level [supabase/storage-js#151](https://github.com/supabase/storage-js/pull/151) file size and mime type
|
||||
limits per bucket
|
||||
- feat: quality option, image
|
||||
transformation [supabase/storage-js#145](https://github.com/supabase/storage-js/pull/152) quality option for
|
||||
image transformations
|
||||
- feat: format option for webp
|
||||
support [supabase/storage-js#142](https://github.com/supabase/storage-js/pull/142) format option for image
|
||||
transformation
|
||||
|
||||
## 0.8.8 - 2023-03-29
|
||||
|
||||
- Update dependency: gotrue-csharp@3.0.6
|
||||
- Supports adding `SignInOptions` (i.e. `RedirectTo`) on `OAuth Provider` SignIn requests.
|
||||
|
||||
## 0.8.7 - 2023-03-23
|
||||
|
||||
- Update dependency: realtime-csharp@5.0.4
|
||||
- Re: [#26](https://github.com/supabase-community/realtime-csharp/pull/26) - Fixes Connect() not returning callback
|
||||
result when the socket isn't null. Thanks [@BlueWaterCrystal](https://github.com/BlueWaterCrystal)!
|
||||
|
||||
## 0.8.6 - 2023-03-23
|
||||
|
||||
- Update dependency: supabase-storage-csharp@1.2.8
|
||||
- [Merge #5](https://github.com/supabase-community/storage-csharp/pull/5) Added search string as an optional search
|
||||
parameter. Thanks [@ElectroKnight22](https://github.com/ElectroKnight22)!
|
||||
|
||||
## 0.8.5 - 2023-03-10
|
||||
|
||||
- Update dependency: realtime-csharp@5.0.3
|
||||
- Re: [#25](https://github.com/supabase-community/realtime-csharp/issues/25) - Support Channel being resubscribed
|
||||
after having been unsubscribed, fixes rejoin timer being erroneously called on channel `Unsubscribe`.
|
||||
Thanks [@Kuffs2205](https://github.com/Kuffs2205)!
|
||||
|
||||
## 0.8.4 - 2023-03-03
|
||||
|
||||
- Update dependency: supabase-storage-csharp@1.2.7
|
||||
- Re: [#4](https://github.com/supabase-community/storage-csharp/issues/4) Implementation for `ClientOptions` which
|
||||
supports specifying Upload, Download, and Request timeouts.
|
||||
- Update dependency: realtime-csharp@5.0.2
|
||||
- Re: [#24](https://github.com/supabase-community/realtime-csharp/issues/24) - Fixes join failing until reconnect
|
||||
happened + adds access token push on channel join. Big thank you to [@Honeyhead](https://github.com/honeyhead) for
|
||||
the help debugging and identifying!
|
||||
|
||||
## 0.8.3 - 2023-02-26
|
||||
|
||||
- Update dependency: supabase-storage-csharp@1.2.5
|
||||
- Provides fix
|
||||
for [supabase-community/supabase-csharp#54](https://github.com/supabase-community/supabase-csharp/issues/54) -
|
||||
Dynamic headers were always being overwritten by initialized token headers, so the storage client would not
|
||||
receive user's access token as expected.
|
||||
- Provides fix for upload progress not reporting
|
||||
in [supabase-community/storage-csharp#3](https://github.com/supabase-community/storage-csharp/issues/3)
|
||||
- Update dependency: gotrue-csharp@3.0.5
|
||||
- Fixes [#44](https://github.com/supabase-community/gotrue-csharp/issues/44) - refresh timer should automatically
|
||||
reattempt (interval of 5s) for HTTP exceptions - gracefully exits on invalid refresh and triggers
|
||||
an `AuthState.Changed` event
|
||||
|
||||
## 0.8.2 - 2023-02-26
|
||||
|
||||
- Update dependency: supabase-storage-csharp@1.2.4
|
||||
- `UploadOrUpdate` now appropriately throws request exceptions
|
||||
|
||||
## 0.8.1 - 2023-02-06
|
||||
|
||||
- Update dependency: realtime-csharp@5.0.1
|
||||
- Re: [#22](https://github.com/supabase-community/realtime-csharp/issues/22) - `SerializerSettings` were not being
|
||||
passed to `PostgresChangesResponse` - Thanks [@Shenrak](https://github.com/Shenrak) for the help debugging!
|
||||
|
||||
## 0.8.0 - 2023-01-31
|
||||
|
||||
- Update dependency: realtime-csharp@5.0.0
|
||||
|
||||
@@ -41,7 +41,7 @@ _Optionally_ if you are using custom configuration with `createClient` then foll
|
||||
>
|
||||
<TabPanel id="1.0x" label="Before">
|
||||
|
||||
```ts title=src/supabaseClient.ts
|
||||
```ts src/supabaseClient.ts
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
schema: 'custom',
|
||||
persistSession: false,
|
||||
@@ -51,7 +51,7 @@ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
</TabPanel>
|
||||
<TabPanel id="2.0x" label="After">
|
||||
|
||||
```ts title=src/supabaseClient.ts
|
||||
```ts src/supabaseClient.ts
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
db: {
|
||||
schema: 'custom',
|
||||
|
||||
@@ -10,7 +10,7 @@ For `supabase-flutter`, you will be using the static `initialize()` method on `S
|
||||
|
||||
### Flutter `initialize()`
|
||||
|
||||
```dart title=main.dart
|
||||
```dart main.dart
|
||||
Future<void> main() async {
|
||||
await Supabase.initialize(url: 'https://xyzcompany.supabase.co', anonKey: 'public-anon-key');
|
||||
runApp(MyApp());
|
||||
@@ -30,7 +30,7 @@ final supabase = Supabase.instance.client;
|
||||
You can pass `headers` to initialize your Supabase client with customer headers.
|
||||
Here is an example of passing a custom auth header to Supabase client.
|
||||
|
||||
```dart title=main.dart
|
||||
```dart main.dart
|
||||
Future<void> main() async {
|
||||
await Supabase.initialize(
|
||||
url: 'https://xyzcompany.supabase.co',
|
||||
|
||||
@@ -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
|
||||
@@ -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',
|
||||
|
||||
@@ -137,14 +137,12 @@ const Layout: FC<Props> = (props) => {
|
||||
) : (
|
||||
<div className="mt-16 not-prose">
|
||||
<div>
|
||||
<Link
|
||||
<a
|
||||
href={`https://github.com/supabase/supabase/edit/master/apps/docs/pages${router.asPath}.mdx`}
|
||||
passHref
|
||||
className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200"
|
||||
>
|
||||
<a className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200">
|
||||
Edit this page on GitHub <IconExternalLink size={14} strokeWidth={1.5} />
|
||||
</a>
|
||||
</Link>
|
||||
Edit this page on GitHub <IconExternalLink size={14} strokeWidth={1.5} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -132,7 +132,7 @@ const StickyHeader: FC<StickyHeader> = ({ icon, ...props }) => {
|
||||
}
|
||||
|
||||
const Details: FC<ISectionDetails> = (props) => {
|
||||
return <div className="relative overflow-hidden w-full">{props.children}</div>
|
||||
return <div className="relative w-full">{props.children}</div>
|
||||
}
|
||||
|
||||
const Examples: FC<ISectionExamples> = (props) => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
|
||||
import matter from 'gray-matter'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
|
||||
import { ICommonMarkdown } from '~/components/reference/Reference.types'
|
||||
|
||||
async function generateRefMarkdown(sections: ICommonMarkdown[], slug: string) {
|
||||
@@ -31,6 +33,14 @@ async function generateRefMarkdown(sections: ICommonMarkdown[], slug: string) {
|
||||
const fileContents = markdownExists ? fs.readFileSync(pathName, 'utf8') : ''
|
||||
const { data, content } = matter(fileContents)
|
||||
|
||||
const codeHikeOptions: CodeHikeConfig = {
|
||||
theme: codeHikeTheme,
|
||||
lineNumbers: true,
|
||||
showCopyButton: true,
|
||||
skipLanguages: [],
|
||||
autoImport: false,
|
||||
}
|
||||
|
||||
markdownContent.push({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
@@ -41,8 +51,8 @@ async function generateRefMarkdown(sections: ICommonMarkdown[], slug: string) {
|
||||
// MDX's available options, see the MDX docs for more info.
|
||||
// https://mdxjs.com/packages/mdx/#compilefile-options
|
||||
mdxOptions: {
|
||||
// remarkPlugins: [[remarkCodeHike, { autoImport: false, theme }]],
|
||||
useDynamicImport: true,
|
||||
remarkPlugins: [[remarkCodeHike, codeHikeOptions]],
|
||||
},
|
||||
// Indicates whether or not to parse the frontmatter from the mdx source
|
||||
})
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Element } from 'hast'
|
||||
import { hasProperty } from 'hast-util-has-property'
|
||||
import { Node } from 'unist'
|
||||
import { visit } from 'unist-util-visit'
|
||||
|
||||
export type UrlTransformFunction = (url: string, node: Element) => string
|
||||
|
||||
function modify(node: Element, prop: string, fn?: UrlTransformFunction) {
|
||||
if (hasProperty(node, prop)) {
|
||||
const property = node.properties[prop]
|
||||
if (typeof property !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
node.properties[prop] = fn?.(property, node) ?? property
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms every HAST element that contains a `href` or `src`.
|
||||
* A `UrlTransformFunction` is called with the current URL. The
|
||||
* return value from this function will be used as the replacement.
|
||||
*/
|
||||
export function linkTransform(fn?: UrlTransformFunction) {
|
||||
return function transformer(tree: Node) {
|
||||
visit(tree, 'element', (node: Element) => {
|
||||
modify(node, 'href', fn)
|
||||
modify(node, 'src', fn)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Content, Paragraph, Parent } from 'mdast'
|
||||
import { MdxJsxFlowElement } from 'mdast-util-mdx'
|
||||
import { Node } from 'unist'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { AdmonitionProps } from '~/components/Admonition'
|
||||
|
||||
/**
|
||||
* Transforms an `mkdocs-material` Admonition to a Supabase Admonition.
|
||||
*
|
||||
* https://squidfunk.github.io/mkdocs-material/reference/admonitions/
|
||||
*/
|
||||
const remarkMkDocsAdmonition = function () {
|
||||
return function transformer(root: Parent) {
|
||||
visit(root, 'paragraph', (paragraph: Paragraph, index: number, parent: Parent) => {
|
||||
const [firstChild] = paragraph.children
|
||||
|
||||
if (firstChild?.type === 'text') {
|
||||
const match = firstChild.value.match(/^!!! ?(.*?)\n(.*)/s)
|
||||
|
||||
if (!match) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the admonition type along with the remaining text
|
||||
const [, type, value] = match
|
||||
|
||||
// Rewrite the node's value to remove the admonition syntax
|
||||
firstChild.value = value
|
||||
|
||||
// Extract sibling nodes that should be linked to this admonition
|
||||
const siblingsToNest = extractLinkedSiblings(parent, paragraph, index)
|
||||
|
||||
const children: any[] = [...paragraph.children, ...siblingsToNest]
|
||||
|
||||
// Generate a Supabase Admonition JSX element
|
||||
const admonitionElement: MdxJsxFlowElement = {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'Admonition',
|
||||
attributes: [
|
||||
{
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'type',
|
||||
value: mapAdmonitionType(type),
|
||||
},
|
||||
],
|
||||
children,
|
||||
}
|
||||
|
||||
// Overwrite original node with new element
|
||||
parent.children.splice(index, 1, admonitionElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies sibling nodes that should be linked to this admonition
|
||||
* based on their indent level (ie. 4 spaces).
|
||||
*
|
||||
* Iterates through proceeding siblings until one is found that is
|
||||
* not indented relative to the original node.
|
||||
*
|
||||
* Splices the discovered siblings out of the original parent and returns them.
|
||||
*/
|
||||
function extractLinkedSiblings(parent: Parent, node: Node, index: number, indentAmount = 4) {
|
||||
const { column } = node.position.start
|
||||
|
||||
let nextSibling: Content
|
||||
let i = index
|
||||
|
||||
do {
|
||||
nextSibling = parent.children[++i]
|
||||
} while (nextSibling?.position && nextSibling.position.start.column === column + indentAmount)
|
||||
|
||||
return parent.children.splice(index + 1, i - index - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps `mkdocs-material` Admonition types to Supabase Admonition types.
|
||||
*
|
||||
* https://squidfunk.github.io/mkdocs-material/reference/admonitions/#supported-types
|
||||
*/
|
||||
function mapAdmonitionType(type: string): AdmonitionProps['type'] {
|
||||
switch (type) {
|
||||
case 'quote':
|
||||
case 'example':
|
||||
case 'note':
|
||||
return 'note'
|
||||
case 'tip':
|
||||
return 'tip'
|
||||
case 'warning':
|
||||
return 'caution'
|
||||
case 'failure':
|
||||
case 'bug':
|
||||
case 'danger':
|
||||
return 'danger'
|
||||
case 'abstract':
|
||||
case 'question':
|
||||
case 'info':
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
export default remarkMkDocsAdmonition
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Parent } from 'mdast'
|
||||
|
||||
/**
|
||||
* Removes the top heading from a MD file if
|
||||
* it is the first node and it matches `title`.
|
||||
*
|
||||
* Useful when rendering title separately from MD
|
||||
* and you need to remove the duplicate.
|
||||
*/
|
||||
export function removeTitle(title: string) {
|
||||
return function transformer(root: Parent) {
|
||||
const [firstNode] = root.children
|
||||
|
||||
if (firstNode?.type === 'heading') {
|
||||
const [text] = firstNode.children
|
||||
|
||||
if (text?.type === 'text' && text.value === title) {
|
||||
// Remove this node
|
||||
root.children.splice(0, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-20
@@ -1,22 +1,19 @@
|
||||
// @ts-check
|
||||
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
|
||||
@@ -28,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',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -61,10 +56,12 @@ const nextConfig = {
|
||||
'raw.githubusercontent.com',
|
||||
'weweb-changelog.ghost.io',
|
||||
'img.youtube.com',
|
||||
'archbee-image-uploads.s3.amazonaws.com',
|
||||
],
|
||||
},
|
||||
experimental: {
|
||||
mdxRs: true,
|
||||
// TODO: @next/mdx ^13.0.2 only supports experimental mdxRs flag. next ^13.0.2 will stop warning about this being unsupported.
|
||||
// mdxRs: true,
|
||||
modularizeImports: {
|
||||
lodash: {
|
||||
transform: 'lodash/{{member}}',
|
||||
@@ -106,7 +103,15 @@ const nextConfig = {
|
||||
|
||||
const configExport = () => {
|
||||
const plugins = [
|
||||
withTM(['ui', 'common', '@supabase/auth-helpers-nextjs']),
|
||||
withTM([
|
||||
'ui',
|
||||
'common',
|
||||
'@supabase/auth-helpers-nextjs',
|
||||
'mermaid',
|
||||
'mdx-mermaid',
|
||||
'dayjs',
|
||||
'shared-data',
|
||||
]),
|
||||
withMDX,
|
||||
withYaml,
|
||||
withBundleAnalyzer,
|
||||
|
||||
+24
-17
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -3,9 +3,7 @@ import { Html, Head, Main, NextScript } from 'next/document'
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
|
||||
</Head>
|
||||
<Head />
|
||||
<body className="dark">
|
||||
<Main />
|
||||
<NextScript />
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai',
|
||||
title: 'AI & Vectors',
|
||||
description: 'The best vector database is the database you already have.',
|
||||
subtitle: 'The best vector database is the database you already have.',
|
||||
sidebar_label: 'Overview',
|
||||
}
|
||||
|
||||
Supabase provides an open source toolkit for developing AI applications using Postgres and pgvector. Use the Supabase client libraries to store, index, and query your vector embeddings at scale.
|
||||
|
||||
The toolkit includes:
|
||||
|
||||
- A [vector store](/docs/guides/ai/vector-columns) and embeddings support using Postgres and pgvector.
|
||||
- A [Python client](/docs/guides/ai/vecs-python-client) for managing unstructured embeddings.
|
||||
- [Database migrations](/docs/guides/ai/examples/headless-vector-search#prepare-your-database) for managing structured embeddings.
|
||||
- Integrations with all popular AI providers, such as [OpenAI](/docs/guides/ai/examples/openai), [Hugging Face](/docs/guides/ai/hugging-face), [LangChain](/docs/guides/ai/langchain), and more.
|
||||
|
||||
## Examples
|
||||
|
||||
Check out all of the AI [templates and examples](https://github.com/supabase/supabase/tree/master/examples/ai) in our GitHub repository.
|
||||
|
||||
<div className="grid md:grid-cols-12 gap-4 not-prose">
|
||||
{examples.map((x) => (
|
||||
<div className="col-span-4" key={x.href}>
|
||||
<Link href={x.href} passHref>
|
||||
<a>
|
||||
<GlassPanel icon={'/docs/img/icons/github-icon'} hasLightIcon={true} title={x.name}>
|
||||
{x.description}
|
||||
</GlassPanel>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
export const examples = [
|
||||
{
|
||||
name: 'Headless Vector Search',
|
||||
description: 'A toolkit to perform vector similarity search on your knowledge base embeddings.',
|
||||
href: '/guides/ai/examples/headless-vector-search',
|
||||
},
|
||||
{
|
||||
name: 'Image Search with OpenAI CLIP',
|
||||
description: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.',
|
||||
href: '/guides/ai/examples/image-search-openai-clip',
|
||||
},
|
||||
{
|
||||
name: 'Hugging Face inference',
|
||||
description: 'Generate image captions using Hugging Face.',
|
||||
href: '/guides/ai/examples/huggingface-image-captioning',
|
||||
},
|
||||
{
|
||||
name: 'OpenAI completions',
|
||||
description: 'Generate GPT text completions using OpenAI in Edge Functions.',
|
||||
href: '/guides/ai/examples/openai',
|
||||
},
|
||||
{
|
||||
name: 'Building ChatGPT Plugins',
|
||||
description: 'Use Supabase as a Retrieval Store for your ChatGPT plugin.',
|
||||
href: '/guides/ai/examples/building-chatgpt-plugins',
|
||||
},
|
||||
{
|
||||
name: 'Vector search with Next.js and OpenAI',
|
||||
description:
|
||||
'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.',
|
||||
href: '/guides/ai/examples/nextjs-vector-search',
|
||||
},
|
||||
]
|
||||
|
||||
## Integrations
|
||||
|
||||
<div className="grid md:grid-cols-12 gap-4 not-prose">
|
||||
{integrations.map((x) => (
|
||||
<div className="col-span-4" key={x.href}>
|
||||
<Link href={x.href} passHref>
|
||||
<a>
|
||||
<GlassPanel title={x.name}>{x.description}</GlassPanel>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
export const integrations = [
|
||||
{
|
||||
name: 'OpenAI',
|
||||
description:
|
||||
'OpenAI is an AI research and deployment company. Supabase provides a simple way to use OpenAI in your applications.',
|
||||
href: '/guides/ai/examples/building-chatgpt-plugins',
|
||||
},
|
||||
{
|
||||
name: 'Hugging Face',
|
||||
description:
|
||||
"Hugging Face is an open-source provider of NLP technologies. Supabase provides a simple way to use Hugging Face's models in your applications.",
|
||||
href: '/guides/ai/hugging-face',
|
||||
},
|
||||
{
|
||||
name: 'LangChain',
|
||||
description:
|
||||
'LangChain is a language-agnostic, open-source, and self-hosted API for text translation, summarization, and sentiment analysis.',
|
||||
href: '/guides/ai/langchain',
|
||||
},
|
||||
{
|
||||
name: 'LlamaIndex',
|
||||
description: 'LlamaIndex is a data framework for your LLM applications.',
|
||||
href: '/guides/ai/integrations/llamaindex',
|
||||
},
|
||||
]
|
||||
|
||||
## Case studies
|
||||
|
||||
<div className="grid md:grid-cols-12 gap-4 not-prose">
|
||||
{customers.map((x) => (
|
||||
<div className="col-span-4" key={x.href}>
|
||||
<Link href={x.href} passHref>
|
||||
<a>
|
||||
<GlassPanel title={x.name}>{x.description}</GlassPanel>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
export const customers = [
|
||||
{
|
||||
name: 'Berri AI Boosts Productivity by Migrating from AWS RDS to Supabase with pgvector',
|
||||
description:
|
||||
'Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase.',
|
||||
href: 'https://supabase.com/customers/berriai',
|
||||
},
|
||||
{
|
||||
name: 'Mendable switches from Pinecone to Supabase for PostgreSQL vector embeddings',
|
||||
description:
|
||||
'How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector',
|
||||
href: 'https://supabase.com/customers/mendableai',
|
||||
},
|
||||
{
|
||||
name: 'Markprompt: GDPR-Compliant AI Chatbots for Docs and Websites',
|
||||
description:
|
||||
"AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure and privacy-focused database and authentication solutions",
|
||||
href: 'https://supabase.com/customers/markprompt',
|
||||
},
|
||||
]
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,67 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-concepts',
|
||||
title: 'Concepts',
|
||||
description: 'Learn about embeddings within AI and vector applications.',
|
||||
sidebar_label: 'Concepts',
|
||||
}
|
||||
|
||||
Embeddings are a core concept when building AI and vector applications.
|
||||
|
||||
## What are embeddings?
|
||||
|
||||
Embeddings capture the "relatedness" of text, images, video, or other types of information. This relatedness is most commonly used for:
|
||||
|
||||
- **Search:** how similar is a search term to a body of text?
|
||||
- **Recommendations:** how similar are two products?
|
||||
- **Classifications:** how do we categorize a body of text?
|
||||
- **Clustering:** how do we identify trends?
|
||||
|
||||
Let's explore an example of text embeddings. Say we have three phrases:
|
||||
|
||||
1. "The cat chases the mouse"
|
||||
2. "The kitten hunts rodents"
|
||||
3. "I like ham sandwiches"
|
||||
|
||||
Your job is to group phrases with similar meaning. If you are a human, this should be obvious. Phrases 1 and 2 are almost identical, while phrase 3 has a completely different meaning.
|
||||
|
||||
Although phrases 1 and 2 are similar, they share no common vocabulary (besides "the"). Yet their meanings are nearly identical. How can we teach a computer that these are the same?
|
||||
|
||||
## Human language
|
||||
|
||||
Humans use words and symbols to communicate language. But words in isolation are mostly meaningless - we need to draw from shared knowledge & experience in order to make sense of them. The phrase “You should Google it” only makes sense if you know that Google is a search engine and that people have been using it as a verb.
|
||||
|
||||
In the same way, we need to train a neural network model to understand human language. An effective model should be trained on millions of different examples to understand what each word, phrase, sentence, or paragraph could mean in different contexts.
|
||||
|
||||
So how does this relate to embeddings?
|
||||
|
||||
## How do embeddings work?
|
||||
|
||||
Embeddings compress discrete information (words & symbols) into distributed continuous-valued data (vectors). If we took our phrases from before and plot them on a chart, it might look something like this:
|
||||
|
||||
<img src="/docs/img/ai/vector-similarity.png" alt="Vector similarity" width="640" height="640" />
|
||||
|
||||
Phrases 1 and 2 would be plotted close to each other, since their meanings are similar. We would expect phrase 3 to live somewhere far away since it isn't related. If we had a fourth phrase, “Sally ate Swiss cheese”, this might exist somewhere between phrase 3 (cheese can go on sandwiches) and phrase 1 (mice like Swiss cheese).
|
||||
|
||||
In this example we only have 2 dimensions: the X and Y axis. In reality, we would need many more dimensions to effectively capture the complexities of human language.
|
||||
|
||||
## Using embeddings
|
||||
|
||||
Compared to our 2-dimensional example above, most embedding models will output many more dimensions. For example OpenAI's `text-embedding-ada-002` model outputs 1536 dimensions.
|
||||
|
||||
Why is this useful? Once we have generated embeddings on multiple texts, it is trivial to calculate how similar they are using vector math operations like cosine distance. A common use case for this is search. Your process might look something like this:
|
||||
|
||||
1. Pre-process your knowledge base and generate embeddings for each page
|
||||
2. Store your embeddings to be referenced later
|
||||
3. Build a search page that prompts your user for input
|
||||
4. Take user's input, generate a one-time embedding, then perform a similarity search against your pre-processed embeddings.
|
||||
5. Return the most similar pages to the user
|
||||
|
||||
## See also
|
||||
|
||||
- [Structured and Unstructured embeddings](/docs/guides/ai/structured-unstructured)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,160 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-engineering-for-scale',
|
||||
title: 'Engineering for Scale',
|
||||
description: 'Building an enterprise-grade vector architecture',
|
||||
subtitle: 'Building an enterprise-grade vector architecture.',
|
||||
sidebar_label: 'Engineering for Scale',
|
||||
}
|
||||
|
||||
Content sources for vectors can be extremely large. As you grow you should run your Vector workloads across several secondary databases (sometimes called "pods"), which allows each collection to scale independently.
|
||||
|
||||
## Simple workloads
|
||||
|
||||
For small workloads it's typical to store your data in a single database.
|
||||
|
||||
If you've used [Vecs](/docs/guides/ai/vecs-python-client) to create 3 different collections, you can expose collections to your web or mobile application using [views](/docs/guides/database/tables#views):
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="single database"
|
||||
className="dark:hidden"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--single-database--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="single database"
|
||||
className="hidden dark:block"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--single-database--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
For example, with 3 collections, called `docs`, `posts`, and `images`, we could expose the "docs" inside the public schema like this:
|
||||
|
||||
```sql
|
||||
create view public.docs as
|
||||
select
|
||||
id,
|
||||
embedding,
|
||||
metadata, # Expose the metadata as JSON
|
||||
(metadata->>'url')::text as url # Extract the URL as a string
|
||||
from vector
|
||||
```
|
||||
|
||||
You can then use any of the client libraries to access your collections within your applications:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```js
|
||||
const { data, error } = await supabase
|
||||
.from('docs')
|
||||
.select('id, embedding, metadata')
|
||||
.eq('url', '/hello-world')
|
||||
```
|
||||
|
||||
## Enterprise workloads
|
||||
|
||||
As you move into production, we recommend running splitting your collections into separate projects. This is because it allows your vector stores to scale independently of your production data. Vectors typically grow faster than operational data, and they have different resource requirements. Running them on separate databases removes the single-point-of-failure.
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="With secondaries"
|
||||
className="dark:hidden"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--with-secondaries--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="With secondaries"
|
||||
className="hidden dark:block"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--with-secondaries--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
You can use as many secondary databases as you need to manage your collections. With this architecture, you have 2 options for accessing collections within your application:
|
||||
|
||||
1. Query the collections directly using Vecs.
|
||||
2. Access the collections from your Primary database through a Wrapper.
|
||||
|
||||
You can use both of these in tandem to suit your use-case. We recommend option `1` wherever possible, as it offers the most scalability.
|
||||
|
||||
### Query collections using Vecs
|
||||
|
||||
Vecs provides methods for querying collections, either using a [cosine similarity function](https://supabase.github.io/vecs/api/#basic) or with [metadata filtering](https://supabase.github.io/vecs/api/#metadata-filtering).
|
||||
|
||||
```python
|
||||
# cosine similarity
|
||||
docs.query(query_vector=[0.4,0.5,0.6], limit=5)
|
||||
|
||||
# metadata filtering
|
||||
docs.query(
|
||||
query_vector=[0.4,0.5,0.6],
|
||||
limit=5,
|
||||
filters={"year": {"$eq": 2012}}, # metadata filters
|
||||
)
|
||||
```
|
||||
|
||||
### Accessing external collections using Wrappers
|
||||
|
||||
Supabase supports [Foreign Data Wrappers](/blog/postgres-foreign-data-wrappers-rust). Wrappers allow you connect two databases together so that you can query them over the network.
|
||||
|
||||
This involves 2 steps: connecting to your remote database from the primary, and creating a Foreign Table.
|
||||
|
||||
#### Connecting your remote database
|
||||
|
||||
Inside your Primary database we need to provide the credentials to access the secondary database:
|
||||
|
||||
```sql
|
||||
create extension postgres_fdw;
|
||||
|
||||
create server docs_server
|
||||
foreign data wrapper postgres_fdw
|
||||
options (host 'db.xxx.supabase.co', port '5432', dbname 'postgres');
|
||||
|
||||
create user mapping for docs_user
|
||||
server docs_server
|
||||
options (user 'postgres', password 'password');
|
||||
```
|
||||
|
||||
#### Create a foreign table
|
||||
|
||||
We can now create a foreign table to access the data in our secondary project.
|
||||
|
||||
```sql
|
||||
create foreign table docs (
|
||||
id text not null,
|
||||
embedding vector(1536),
|
||||
metadata jsonb,
|
||||
url text
|
||||
)
|
||||
server docs_server
|
||||
options (schema_name 'public', table_name 'docs');
|
||||
```
|
||||
|
||||
This looks very similar to our View example above, and you can continue to use the client libraries to access your collections through the foreign table:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```js
|
||||
const { data, error } = await supabase
|
||||
.from('docs')
|
||||
.select('id, embedding, metadata')
|
||||
.eq('url', '/hello-world')
|
||||
```
|
||||
|
||||
### Enterprise architecture
|
||||
|
||||
This diagram provides an example architecture, allowing you to access the collections either with our client libraries or using Vecs. You can add as many secondary databases as you need, in this example we show one only:
|
||||
|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--multi-database--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--multi-database--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,159 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
title: 'Building ChatGPT plugins',
|
||||
subtitle: 'Use Supabase as a Retrieval Store for your ChatGPT plugin.',
|
||||
breadcrumb: 'AI Examples',
|
||||
}
|
||||
|
||||
ChatGPT recently released [Plugins](https://openai.com/blog/chatgpt-plugins) which help ChatGPT access up-to-date information, run computations, or use third-party services.
|
||||
If you're building a plugin for ChatGPT, you'll probably want to answer questions from a specific source. We can solve this with “retrieval plugins”, which allow ChatGPT to access information from a database.
|
||||
|
||||
## What is ChatGPT Retrieval Plugin?
|
||||
|
||||
A [Retrieval Plugin](https://github.com/openai/chatgpt-retrieval-plugin) is a Python project designed to inject external data into a ChatGPT conversation. It does a few things:
|
||||
|
||||
1. Turn documents into smaller chunks.
|
||||
2. Converts chunks into embeddings using OpenAI's `text-embedding-ada-002` model.
|
||||
3. Stores the embeddings into a vector database.
|
||||
4. Queries the vector database for relevant documents when a question is asked.
|
||||
|
||||
It allows ChatGPT to dynamically pull relevant information into conversations from your data sources. This could be PDF documents, Confluence, or Notion knowledge bases.
|
||||
|
||||
## Example: Chat with Postgres Docs
|
||||
|
||||
Let’s build an example where we can “ask ChatGPT questions” about the Postgres documentation. Although ChatGPT already knows about the Postgres documentation because it is publicly available, this is a simple example which demonstrates how to work with PDF files.
|
||||
|
||||
This plugin requires several steps:
|
||||
|
||||
1. Download all the [Postgres docs as a PDF](https://www.postgresql.org/files/documentation/pdf/15/postgresql-15-US.pdf)
|
||||
2. Convert the docs into chunks of embedded text and store them in Supabase
|
||||
3. Run our plugin locally so that we can ask questions about the Postgres docs.
|
||||
|
||||
We'll be saving the Postgres documentation in Postgres, and ChatGPT will be retrieving the documentation whenever a user asks a question:
|
||||
|
||||
<img
|
||||
className="dark:hidden !m-0"
|
||||
alt="diagram reference"
|
||||
src="/docs/img/ai/chatgpt-plugins/chatgpt-plugin-scheme--light.png"
|
||||
/>
|
||||
|
||||
<img
|
||||
className="hidden dark:block !m-0"
|
||||
alt="diagram reference"
|
||||
src="/docs/img/ai/chatgpt-plugins/chatgpt-plugin-scheme--dark.png"
|
||||
/>
|
||||
|
||||
### Step 1: Fork the ChatGPT Retrieval Plugin repository
|
||||
|
||||
Fork the ChatGPT Retrieval Plugin repository to your GitHub account and clone it to your local machine. Read through the `README.md` file to understand the project structure.
|
||||
|
||||
### Step 2: Install dependencies
|
||||
|
||||
Choose your desired datastore provider and remove unused dependencies from `pyproject.toml`. For this example, we'll use Supabase. And install dependencies with Poetry:
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
### Step 3: Create a Supabase project
|
||||
|
||||
Create a [Supabase project](https://supabase.com/dashboard) and database by following the instructions [here](https://supabase.com/docs/guides/platform). Export the environment variables required for the retrieval plugin to work:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=<open_ai_api_key>
|
||||
export DATASTORE=supabase
|
||||
export SUPABASE_URL=<supabase_url>
|
||||
export SUPABASE_SERVICE_ROLE_KEY=<supabase_key>
|
||||
```
|
||||
|
||||
For Postgres datastore, you'll need to export these environment variables instead:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=<open_ai_api_key>
|
||||
export DATASTORE=postgres
|
||||
export PG_HOST=<postgres_host_url>
|
||||
export PG_PASSWORD=<postgres_password>
|
||||
```
|
||||
|
||||
### Step 4: Run Postgres Locally
|
||||
|
||||
To start quicker you may use Supabase CLI to spin everything up locally as it already includes pgvector from the start. Install `supabase-cli`, go to the `examples/providers` folder in the repo and run:
|
||||
|
||||
```bash
|
||||
supabase start
|
||||
```
|
||||
|
||||
This will pull all docker images and run supabase stack in docker on your local machine. It will also apply all the necessary migrations to set the whole thing up. You can then use your local setup the same way, just export the environment variables and follow to the next steps.
|
||||
|
||||
Using `supabase-cli` is not required and you can use any other docker image or hosted version of PostgresDB that includes `pgvector`. Just make sure you run migrations from `examples/providers/supabase/migrations/20230414142107_init_pg_vector.sql`.
|
||||
|
||||
### Step 5: Obtain OpenAI API key
|
||||
|
||||
To create embeddings Plugin uses OpenAI API and `text-embedding-ada-002` model. Each time we add some data to our datastore, or try to query relevant information from it, embedding will be created either for inserted data chunk, or for the query itself. To make it work we need to export `OPENAI_API_KEY`. If you already have an account in OpenAI, you just need to go to [User Settings - API keys](https://platform.openai.com/account/api-keys) and Create new secret key.
|
||||
|
||||

|
||||
|
||||
### Step 6: Run the plugin
|
||||
|
||||
Execute the following command to run the plugin:
|
||||
|
||||
```bash
|
||||
poetry run dev
|
||||
# output
|
||||
INFO: Will watch for changes in these directories: ['./chatgpt-retrieval-plugin']
|
||||
INFO: Uvicorn running on http://localhost:3333 (Press CTRL+C to quit)
|
||||
INFO: Started reloader process [87843] using WatchFiles
|
||||
INFO: Started server process [87849]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
```
|
||||
|
||||
The plugin will start on your localhost - port `:3333` by default.
|
||||
|
||||
### Step 6: Populating data in the datastore
|
||||
|
||||
For this example, we'll upload Postgres documentation to the datastore. Download the [Postgres documentation](https://www.postgresql.org/files/documentation/pdf/15/postgresql-15-US.pdf) and use the `/upsert-file` endpoint to upload it:
|
||||
|
||||
```bash
|
||||
curl -X POST -F \\"file=@./postgresql-15-US.pdf\\" <http://localhost:3333/upsert-file>
|
||||
```
|
||||
|
||||
The plugin will split your data and documents into smaller chunks automatically. You can view the chunks using the Supabase dashboard or any other SQL client you prefer. For the whole Postgres Documentation I got 7,904 records in my documents table, which is not a lot, but we can try to add index for `embedding` column to speed things up by a little. To do so, you should run the following SQL command:
|
||||
|
||||
```sql
|
||||
create index on documents
|
||||
using ivfflat (embedding vector_ip_ops)
|
||||
with (lists = 10);
|
||||
```
|
||||
|
||||
This will create an index for the inner product distance function. Important to note that it is an approximate index. It will change the logic from performing the exact nearest neighbor search to the approximate nearest neighbor search.
|
||||
|
||||
We are using `lists = 10`, because as a general guideline, you should start looking for optimal lists constant value with the formula: `rows / 1000` when you have less than 1 million records in your table.
|
||||
|
||||
### Step 7: Using our plugin within ChatGPT
|
||||
|
||||
To integrate our plugin with ChatGPT, register it in the ChatGPT dashboard. Assuming you have access to ChatGPT Plugins and plugin development, select the Plugins model in a new chat, then choose "Plugin store" and "Develop your own plugin." Enter `localhost:3333` into the domain input, and your plugin is now part of ChatGPT.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
And after ChatGPT receives a response from the plugin it will answer your question with the data from the documentation.
|
||||
|
||||

|
||||
|
||||
## Resources
|
||||
|
||||
- ChatGPT Retrieval Plugin: [github.com/openai/chatgpt-retrieval-plugin](https://github.com/openai/chatgpt-retrieval-plugin)
|
||||
- ChatGTP Plugins: [official documentation](https://platform.openai.com/docs/plugins/introduction)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
export default Page
|
||||
@@ -0,0 +1,124 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
title: 'Adding generative Q&A for your documentation',
|
||||
subtitle:
|
||||
'Learn how to build a ChatGPT-style doc search powered using our headless search toolkit.',
|
||||
breadcrumb: 'AI Examples',
|
||||
}
|
||||
|
||||
Supabase provides a [Headless Search Toolkit](https://github.com/supabase/headless-vector-search) for adding "Generative Q&A" to your documentation. The toolkit is "headless", so that you can integrate it into your existing website and style it to match your website theme.
|
||||
|
||||
You can see how this works with the Supabase docs. Just him `cmd+k` and "ask" for something like "what are the features of supabase?". You will see that the response is streamed back, using the information provided in the docs:
|
||||
|
||||

|
||||
|
||||
## Tech stack
|
||||
|
||||
- Supabase: Database & Edge Functions.
|
||||
- OpenAI: Embeddings and completions.
|
||||
- GitHub Actions: for ingesting your markdown docs.
|
||||
|
||||
## Toolkit
|
||||
|
||||
This toolkit consists of 2 parts:
|
||||
|
||||
- The [Headless Vector Search](https://github.com/supabase/headless-vector-search) template which you can deploy in your own organization.
|
||||
- A [GitHub Action](https://github.com/supabase/embeddings-generator) which will ingest your markdown files, convert them to embeddings, and store them in your database.
|
||||
|
||||
## Usage
|
||||
|
||||
There are 3 steps to build similarity search inside your documentation:
|
||||
|
||||
1. Prepare your database.
|
||||
2. Ingest your documentation.
|
||||
3. Add a search interface.
|
||||
|
||||
### Prepare your database
|
||||
|
||||
To prepare, create a [new Supabase project](https://database.new) and store the database and API credentials, which you can find in the project [settings](https://app.supabase.com/_/settings).
|
||||
|
||||
Now we can use the [Headless Vector Search](https://github.com/supabase/headless-vector-search#set-up) instructions to set up the database:
|
||||
|
||||
1. Clone the repo to your local machine: `git clone git@github.com:supabase/headless-vector-search.git`
|
||||
2. Link the repo to your remote project: `supabase link --project-ref XXX`
|
||||
3. Apply the database migrations: `supabase db push`
|
||||
4. Set your OpenAI key as a secret: `supabase secrets set OPENAI_KEY=sk-xxx`
|
||||
5. Deploy the Edge Functions: `supabase functions deploy --no-verify-jwt`
|
||||
6. Expose `docs` schema via API in Supabase Dashboard [settings](https://app.supabase.com/project/_/settings/api) > `API Settings` > `Exposed schemas`
|
||||
|
||||
### Ingest your documentation
|
||||
|
||||
Now we need to push your documentation into the database as embeddings. You can do this manually, but to make it easier we've created a [GitHub Action](https://github.com/marketplace/actions/supabase-embeddings-generator) which can update your database every time there is a Pull Request.
|
||||
|
||||
In your knowledge base repository, create a new action called `.github/workflows/generate_embeddings.yml` with the following content:
|
||||
|
||||
```yml
|
||||
name: 'generate_embeddings'
|
||||
on: # run on main branch changes
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: supabase/supabase-embeddings-generator@v0.0.x # Update this to the latest version.
|
||||
with:
|
||||
supabase-url: 'https://your-project-ref.supabase.co' # Update this to your project URL.
|
||||
supabase-service-role-key: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
|
||||
openai-key: ${{ secrets.OPENAI_KEY }}
|
||||
docs-root-path: 'docs' # the path to the root of your md(x) files
|
||||
```
|
||||
|
||||
Make sure to choose the latest version, and set your `SUPABASE_SERVICE_ROLE_KEY` and `OPENAI_KEY` as repository secrets in your repo settings (settings > secrets > actions).
|
||||
|
||||
### Add a search interface
|
||||
|
||||
Now inside your docs, you need to create a search interface. Because this is a headless interface, you can use it with any language. The only requirement is that you send the user query to the `query` Edge Function, which will stream an answer back from OpenAI. It might look something like this:
|
||||
|
||||
```js
|
||||
const onSubmit = (e: Event) => {
|
||||
e.preventDefault()
|
||||
answer.value = ""
|
||||
isLoading.value = true
|
||||
|
||||
const query = new URLSearchParams({ query: inputRef.current!.value })
|
||||
const projectUrl = `https://your-project-ref.functions.supabase.co`
|
||||
const queryURL = `${projectURL}/${query}`
|
||||
const eventSource = new EventSource(queryURL)
|
||||
|
||||
eventSource.addEventListener("error", (err) => {
|
||||
isLoading.value = false
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
eventSource.addEventListener("message", (e: MessageEvent) => {
|
||||
isLoading.value = false
|
||||
|
||||
if (e.data === "[DONE]") {
|
||||
eventSource.close()
|
||||
return
|
||||
}
|
||||
|
||||
const completionResponse: CreateCompletionResponse = JSON.parse(e.data)
|
||||
const text = completionResponse.choices[0].text
|
||||
|
||||
answer.value += text
|
||||
});
|
||||
|
||||
isLoading.value = true
|
||||
}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Read about how we built [ChatGPT for the Supabase Docs](https://supabase.com/blog/chatgpt-supabase-docs).
|
||||
- Read the pgvector Docs for [Embeddings and vector similarity](/docs/guides/database/extensions/pgvector)
|
||||
- See how to build something like this from scratch [using Next.js](/docs/guides/ai/examples/nextjs-vector-search).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
export default Page
|
||||
@@ -0,0 +1,99 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Generate image captions using Hugging Face',
|
||||
description:
|
||||
'Use the Hugging Face Inference API to make calls to 100,000+ Machine Learning models from Supabase Edge Functions.',
|
||||
subtitle:
|
||||
'Use the Hugging Face Inference API to make calls to 100,000+ Machine Learning models from Supabase Edge Functions.',
|
||||
video: 'https://www.youtube.com/v/OgnYxRkxEUw',
|
||||
tocVideo: 'OgnYxRkxEUw',
|
||||
}
|
||||
|
||||
We can combine Hugging Face with [Supabase Storage](https://supabase.com/storage) and [Database Webhooks](https://supabase.com/docs/guides/database/webhooks) to automatically caption for any image we upload to a storage bucket.
|
||||
|
||||
## About Hugging Face
|
||||
|
||||
[Hugging Face](https://huggingface.co/) is the collaboration platform for the machine learning community.
|
||||
|
||||
[Huggingface.js](https://huggingface.co/docs/huggingface.js/index) provides a convenient way to make calls to 100,000+ Machine Learning models, making it easy to incorporate AI functionality into your [Supabase Edge Functions](https://supabase.com/edge-functions).
|
||||
|
||||
## Setup
|
||||
|
||||
- Open your Supabase project dashboard or [create a new project](https://app.supabase.com/projects).
|
||||
- [Create a new bucket](https://app.supabase.com/project/_/storage/buckets) called `images`.
|
||||
- Generate TypeScript types from remote Database.
|
||||
- Create a new Database table called `image_caption`.
|
||||
- Create `id` column of type `uuid` which references `storage.objects.id`.
|
||||
- Create a `caption` column of type `text`.
|
||||
- Regenerate TypeScript types to include new `image_caption` table.
|
||||
- Deploy the function to Supabase: `supabase functions deploy huggingface-image-captioning`.
|
||||
- Create the Database Webhook in the [Supabase Dashboard](https://app.supabase.com/project/_/database/hooks) to trigger the `huggingface-image-captioning` function anytime a record is added to the `storage.objects` table.
|
||||
|
||||
## Generate TypeScript Types
|
||||
|
||||
To generate the types.ts file for the storage and public schemas, run the following command in the terminal:
|
||||
|
||||
```bash
|
||||
supabase gen types typescript --project-id=your-project-ref --schema=storage,public > supabase/functions/huggingface-image-captioning/types.ts
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
Find the complete code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/huggingface-image-captioning).
|
||||
|
||||
```ts
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
import { HfInference } from 'https://esm.sh/@huggingface/inference@2.3.2'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.7.1'
|
||||
import { Database } from './types.ts'
|
||||
|
||||
console.log('Hello from `huggingface-image-captioning` function!')
|
||||
|
||||
const hf = new HfInference(Deno.env.get('HUGGINGFACE_ACCESS_TOKEN'))
|
||||
|
||||
type SoRecord = Database['storage']['Tables']['objects']['Row']
|
||||
interface WebhookPayload {
|
||||
type: 'INSERT' | 'UPDATE' | 'DELETE'
|
||||
table: string
|
||||
record: SoRecord
|
||||
schema: 'public'
|
||||
old_record: null | SoRecord
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const soRecord = payload.record
|
||||
const supabaseAdminClient = createClient<Database>(
|
||||
// Supabase API URL - env var exported by default when deployed.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API SERVICE ROLE KEY - env var exported by default when deployed.
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||
)
|
||||
|
||||
// Construct image url from storage
|
||||
const { data, error } = await supabaseAdminClient.storage
|
||||
.from(soRecord.bucket_id!)
|
||||
.createSignedUrl(soRecord.path_tokens!.join('/'), 60)
|
||||
if (error) throw error
|
||||
const { signedUrl } = data
|
||||
|
||||
// Run image captioning with Huggingface
|
||||
const imgDesc = await hf.imageToText({
|
||||
data: await (await fetch(signedUrl)).blob(),
|
||||
model: 'nlpconnect/vit-gpt2-image-captioning',
|
||||
})
|
||||
|
||||
// Store image caption in Database table
|
||||
await supabaseAdminClient
|
||||
.from('image_caption')
|
||||
.insert({ id: soRecord.id!, caption: imgDesc.generated_text })
|
||||
.throwOnError()
|
||||
|
||||
return new Response('ok')
|
||||
})
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,177 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'examples-image-search-python',
|
||||
title: 'Image Search with OpenAI CLIP',
|
||||
description: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.',
|
||||
subtitle: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.',
|
||||
}
|
||||
|
||||
The [OpenAI CLIP Model](https://github.com/openai/CLIP) was trained on a variety of (image, text)-pairs. You can use the CLIP model for:
|
||||
|
||||
- Text-to-Image / Image-To-Text / Image-to-Image / Text-to-Text Search
|
||||
- You can fine-tune it on your own image and text data with the regular SentenceTransformers training code.
|
||||
|
||||
[SentenceTransformers](https://www.sbert.net/examples/applications/image-search/README.html) provides models that allow you to embed images and text into the same vector space. You can use this to find similar images as well as to implement image search.
|
||||
|
||||
You can find the full application code as a Python Poetry project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/image_search#image-search-with-supabase-vector).
|
||||
|
||||
## Create a new Python Project with Poetry
|
||||
|
||||
[Poetry](https://python-poetry.org/) provides packaging and dependency management for Python. If you haven't already, install poetry via pip:
|
||||
|
||||
```shell
|
||||
pip install poetry
|
||||
```
|
||||
|
||||
Then initialize a new project:
|
||||
|
||||
```shell
|
||||
poetry new image-search
|
||||
```
|
||||
|
||||
## Setup Supabase project
|
||||
|
||||
If you haven't already, [install the Supabase CLI](/docs/guides/cli), then initialize Supabase in the root of your newly created poetry project:
|
||||
|
||||
```shell
|
||||
supabase init
|
||||
```
|
||||
|
||||
Next, start your local Supabase stack:
|
||||
|
||||
```shell
|
||||
supabase start
|
||||
```
|
||||
|
||||
This will start up the Supabase stack locally and print out a bunch of environtment details, including your local `DB URL`. Make a note of that for later user.
|
||||
|
||||
## Install the Dependencies
|
||||
|
||||
We will need to add the following dependencies to our project:
|
||||
|
||||
- [`vecs`](https://github.com/supabase/vecs#vecs): Supabase Vector Python Client.
|
||||
- [`sentence-transformers`](https://huggingface.co/sentence-transformers/clip-ViT-B-32): a framework for sentence, text and image embeddings (used with OpenAI CLIP model)
|
||||
- [`matplotlib`](https://matplotlib.org/): for displaying our image result
|
||||
|
||||
```shell
|
||||
poetry add vecs sentence-transformers matplotlib
|
||||
```
|
||||
|
||||
## Import the necessary dependencies
|
||||
|
||||
At the top of your main python script, import the dependencies and store your `DB URL` from above in a variable:
|
||||
|
||||
```python
|
||||
from PIL import Image
|
||||
from sentence_transformers import SentenceTransformer
|
||||
import vecs
|
||||
from matplotlib import pyplot as plt
|
||||
from matplotlib import image as mpimg
|
||||
|
||||
DB_CONNECTION = "postgresql://postgres:postgres@localhost:54322/postgres"
|
||||
```
|
||||
|
||||
## Create embeddings for your images
|
||||
|
||||
In the root of your project, create a new folder called `images` and add some images. You can use the images from the example project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/image_search/images) or you can find license free images on [unsplash](https://unsplash.com).
|
||||
|
||||
Next, create a `seed` method, which will create a new Supabase Vector Collection, generate embeddings for your images, and upsert the embeddings into your database:
|
||||
|
||||
```python
|
||||
def seed():
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
|
||||
# create a collection of vectors with 3 dimensions
|
||||
images = vx.create_collection(name="image_vectors", dimension=512)
|
||||
|
||||
# Load CLIP model
|
||||
model = SentenceTransformer('clip-ViT-B-32')
|
||||
|
||||
# Encode an image:
|
||||
img_emb1 = model.encode(Image.open('./images/one.jpg'))
|
||||
img_emb2 = model.encode(Image.open('./images/two.jpg'))
|
||||
img_emb3 = model.encode(Image.open('./images/three.jpg'))
|
||||
img_emb4 = model.encode(Image.open('./images/four.jpg'))
|
||||
|
||||
# add records to the *images* collection
|
||||
images.upsert(
|
||||
vectors=[
|
||||
(
|
||||
"one.jpg", # the vector's identifier
|
||||
img_emb1, # the vector. list or np.array
|
||||
{"type": "jpg"} # associated metadata
|
||||
), (
|
||||
"two.jpg",
|
||||
img_emb2,
|
||||
{"type": "jpg"}
|
||||
), (
|
||||
"three.jpg",
|
||||
img_emb3,
|
||||
{"type": "jpg"}
|
||||
), (
|
||||
"four.jpg",
|
||||
img_emb4,
|
||||
{"type": "jpg"}
|
||||
)
|
||||
]
|
||||
)
|
||||
print("Inserted images")
|
||||
|
||||
# index the collection for fast search performance
|
||||
images.create_index()
|
||||
print("Created index")
|
||||
```
|
||||
|
||||
Add this method as a script in your `pyproject.toml` file:
|
||||
|
||||
```toml
|
||||
[tool.poetry.scripts]
|
||||
seed = "image_search.main:seed"
|
||||
search = "image_search.main:search"
|
||||
```
|
||||
|
||||
After activating the virtual environtment with `poetry shell` you can now run your seed script via `poetry run seed`. You can inspect the generated embeddings in your local database by visiting the local Supabase dashboard at [localhost:54323](http://localhost:54323/project/default/editor), selecting the `vecs` schema, and the `image_vectors` database.
|
||||
|
||||
## Perform an Image Search from a Text Query
|
||||
|
||||
With Supabase Vector we can easily query our embeddings. We can use either an image as search input or alternative we can generate an embedding from a string input and use that as the query input:
|
||||
|
||||
```python
|
||||
def search():
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
images = vx.get_collection(name="image_vectors")
|
||||
|
||||
# Load CLIP model
|
||||
model = SentenceTransformer('clip-ViT-B-32')
|
||||
# Encode text query
|
||||
query_string = "a bike in front of a red brick wall"
|
||||
text_emb = model.encode(query_string)
|
||||
|
||||
# query the collection filtering metadata for "type" = "jpg"
|
||||
results = images.query(
|
||||
query_vector=text_emb, # required
|
||||
limit=1, # number of records to return
|
||||
filters={"type": {"$eq": "jpg"}}, # metadata filters
|
||||
)
|
||||
result = results[0]
|
||||
print(result)
|
||||
plt.title(result)
|
||||
image = mpimg.imread('./images/' + result)
|
||||
plt.imshow(image)
|
||||
plt.show()
|
||||
```
|
||||
|
||||
By limiting the query to one result, we can show the most relevant image to the user. Finally we use `matplotlib` to show the image result to the user.
|
||||
|
||||
That's it, go ahead and test it out by running `poetry run search` and you will be presented with an image of a "bike in front of a red brick wall".
|
||||
|
||||
## Conclusion
|
||||
|
||||
With just a couple of lines of Python you are able to implement image search as well as reverse image search using OpenAI's CLIP model and Supabase Vector.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
+14
-11
@@ -2,20 +2,23 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
title: 'OpenAI Embeddings & Vector Search',
|
||||
title: 'Vector search with Next.js and OpenAI',
|
||||
subtitle:
|
||||
'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.',
|
||||
breadcrumb: 'OpenAI',
|
||||
breadcrumb: 'AI Examples',
|
||||
video: 'https://www.youtube.com/v/xmfNUCjszh4',
|
||||
tocVideo: 'xmfNUCjszh4',
|
||||
}
|
||||
|
||||
In this tutorial we'll look at how you can build a custom ChatGPT-like search experience for your own knowledge base. See our [Supabase Clippy](https://supabase.com/blog/chatgpt-supabase-docs) blog post for an example of how this will look.
|
||||
While our [Headless Vector search](/docs/guides/ai/examples/headless-vector-search) provides a toolkit for generative Q&A, in this tutorial we'll go more in-depth, build a custom ChatGPT-like search experience from the ground-up using Next.js. You will:
|
||||
|
||||
We assume that you have a Next.js project with a collection of `.mdx` files nested inside your `pages` directory. We will start developing locally with the Supabase CLI and then push our local database changes to our hosted Supabase project.
|
||||
1. Convert your markdown into embeddings using OpenAI.
|
||||
2. Store you embeddings in Postgres using pgvector.
|
||||
3. Deploy a function for answering your users' questions.
|
||||
|
||||
<Admonition type="note">
|
||||
You can find the [full example on
|
||||
GitHub](https://github.com/supabase-community/nextjs-openai-doc-search).
|
||||
</Admonition>
|
||||
You can read our [Supabase Clippy](https://supabase.com/blog/chatgpt-supabase-docs) blog post for a full example.
|
||||
|
||||
We assume that you have a Next.js project with a collection of `.mdx` files nested inside your `pages` directory. We will start developing locally with the Supabase CLI and then push our local database changes to our hosted Supabase project. You can find the [full Next.js example on GitHub](https://github.com/supabase-community/nextjs-openai-doc-search).
|
||||
|
||||
## Create a project
|
||||
|
||||
@@ -281,7 +284,7 @@ With our database set up, we need to process and store all `.mdx` files in the `
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```txt
|
||||
```bash
|
||||
NEXT_PUBLIC_SUPABASE_URL=
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||
SUPABASE_SERVICE_ROLE_KEY=
|
||||
@@ -458,7 +461,7 @@ All of this is glued together in a [Vercel Edge Function](https://vercel.com/doc
|
||||
|
||||
In a last step, we need to process the event stream from the OpenAI API and print the answer to the user. The full code for this can be found on [GitHub](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/components/SearchDialog.tsx).
|
||||
|
||||
```tsx
|
||||
```ts
|
||||
const handleConfirm = React.useCallback(
|
||||
async (query: string) => {
|
||||
setAnswer(undefined)
|
||||
@@ -540,5 +543,5 @@ Want to learn more about the awesome tech that is powering this?
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
export default Page
|
||||
+46
-11
@@ -3,20 +3,35 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
export const meta = {
|
||||
id: 'examples-openai',
|
||||
title: 'Generating OpenAI GPT3 completions',
|
||||
description: 'Using OpenAI in Edge Functions.',
|
||||
description: 'Generate GPT text completions using OpenAI and Supabase Edge Functions.',
|
||||
subtitle: 'Generate GPT text completions using OpenAI and Supabase Edge Functions.',
|
||||
video: 'https://www.youtube.com/v/29p8kIqyU_Y',
|
||||
tocVideo: '29p8kIqyU_Y',
|
||||
}
|
||||
|
||||
<div class="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/29p8kIqyU_Y"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
OpenAI provides a [completions API](https://platform.openai.com/docs/api-reference/completions) that allows you to use their generative GPT models in your own applications.
|
||||
|
||||
Use the [OpenAI completions API](https://platform.openai.com/docs/api-reference/completions) in Supabase Edge Functions.
|
||||
OpenAI's API is intended to be used from the server-side. Supabase offers Edge Functions to make it easy to interact with third party APIs like OpenAI.
|
||||
|
||||
## Setup Supabase project
|
||||
|
||||
If you haven't already, [install the Supabase CLI](/docs/guides/cli) and initialize your project:
|
||||
|
||||
```shell
|
||||
supabase init
|
||||
```
|
||||
|
||||
## Create edge function
|
||||
|
||||
Scaffold a new edge function called `openai` by running:
|
||||
|
||||
```shell
|
||||
supabase functions new openai
|
||||
```
|
||||
|
||||
A new edge function will now exist under `./supabase/functions/openai/index.ts`.
|
||||
|
||||
We'll design the function to take your user's query (via POST request) and forward it to OpenAI's API.
|
||||
|
||||
```ts index.ts
|
||||
import 'xhr_polyfill'
|
||||
@@ -31,7 +46,7 @@ serve(async (req) => {
|
||||
prompt: query,
|
||||
max_tokens: 256,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
stream: false,
|
||||
}
|
||||
|
||||
return fetch('https://api.openai.com/v1/completions', {
|
||||
@@ -45,12 +60,28 @@ serve(async (req) => {
|
||||
})
|
||||
```
|
||||
|
||||
Note that we are setting `stream` to `false` which will wait until the entire response is complete before returning. If you wish to stream GPT's response word-by-word back to your client, set `stream` to `true`.
|
||||
|
||||
## Create OpenAI key
|
||||
|
||||
You may have noticed we were passing `OPENAI_API_KEY` in the Authorization header to OpenAI. To generate this key, go to https://platform.openai.com/account/api-keys and create a new secret key.
|
||||
|
||||
After getting the key, copy it into a new file called `.env.local` in your `./supabase` folder:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=your-key-here
|
||||
```
|
||||
|
||||
## Run locally
|
||||
|
||||
Serve the edge function locally by running:
|
||||
|
||||
```bash
|
||||
supabase functions serve --env-file ./supabase/.env.local --no-verify-jwt
|
||||
```
|
||||
|
||||
Notice how we are passing in the `.env.local` file.
|
||||
|
||||
Use cURL or Postman to make a POST request to http://localhost:54321/functions/v1/openai.
|
||||
|
||||
```bash
|
||||
@@ -59,8 +90,12 @@ curl -i --location --request POST http://localhost:54321/functions/v1/openai \
|
||||
--data '{"query":"What is Supabase?"}'
|
||||
```
|
||||
|
||||
You should see a GPT response come back from OpenAI!
|
||||
|
||||
## Deploy
|
||||
|
||||
Deploy your function to the cloud by runnning:
|
||||
|
||||
```bash
|
||||
supabase functions deploy --no-verify-jwt openai
|
||||
supabase secrets set --env-file ./supabase/.env.local
|
||||
@@ -0,0 +1,119 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-google-colab',
|
||||
title: 'Google Colab',
|
||||
description: 'Use Google Colab to manage your Supabase Vector store.',
|
||||
subtitle: 'Use Google Colab to manage your Supabase Vector store.',
|
||||
sidebar_label: 'Google Colab',
|
||||
}
|
||||
|
||||
<a
|
||||
className="w-64"
|
||||
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb"
|
||||
>
|
||||
<img src="/docs/img/ai/colab-badge.svg" />
|
||||
</a>
|
||||
|
||||
Google Colab is a hosted Jupyter Notebook service. It provides free access to computing resources, including GPUs and TPUs, and is well-suited to machine learning, data science, and education. We can use Colab to manage collections using [Supabase Vecs](/docs/guides/ai/vecs-python-client).
|
||||
|
||||
In this tutorial we'll connect to a database running on the Supabase [platform](https://app.supabase.com/). If you don't already have a database, you can create one here: [database.new](https://database.new).
|
||||
|
||||
## Create a new notebook
|
||||
|
||||
Start by visiting [colab.research.google.com](https://colab.research.google.com/). There you can create a new notebook.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Connect to your database
|
||||
|
||||
Find the Postgres connection string for your Supabase project in the [database settings](https://app.supabase.com/_/settings/database) of the dashboard. Copy the "URI" format, which should look something like `postgresql:/postgres:<password>@<host>:5432/postgres`
|
||||
|
||||
Create a new code block below the install block (`ctrl+m b`) and add the following code using the Postgres URI you copied above:
|
||||
|
||||
```py
|
||||
import vecs
|
||||
|
||||
DB_CONNECTION = "postgresql://postgres:<password>@<host>:5432/postgres"
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
```
|
||||
|
||||
Execute the code block (`ctrl+enter`). If no errors were returned then your connection was successful.
|
||||
|
||||
## Create a collection
|
||||
|
||||
Now we're going to create a new collection and insert some documents.
|
||||
|
||||
Create a new code block below the install block (`ctrl+m b`). Add the following code to the code block and execute it (`ctrl+enter`):
|
||||
|
||||
```py
|
||||
collection = vx.create_collection(name="colab_collection", dimension=3)
|
||||
|
||||
collection.upsert(
|
||||
vectors=[
|
||||
(
|
||||
"vec0", # the vector's identifier
|
||||
[0.1, 0.2, 0.3], # the vector. list or np.array
|
||||
{"year": 1973} # associated metadata
|
||||
),
|
||||
(
|
||||
"vec1",
|
||||
[0.7, 0.8, 0.9],
|
||||
{"year": 2012}
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
This will create a table inside your database within the `vecs` schema, called `colab_collection`. You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
|
||||
|
||||

|
||||
|
||||
## 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']`:
|
||||
|
||||

|
||||
|
||||
It also returns a warning:
|
||||
|
||||
```
|
||||
Query does not have a covering index for cosine_distance.
|
||||
```
|
||||
|
||||
You can lean more about creating indexes in the [Vecs documentation](https://supabase.github.io/vecs/api/#create-an-index).
|
||||
|
||||
## Resources
|
||||
|
||||
- Vecs API: [supabase.github.io/vecs/api](https://supabase.github.io/vecs/api)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,172 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-hugging-face',
|
||||
title: 'Hugging Face',
|
||||
description: 'Learn how to integrate hugging face models with Supabase',
|
||||
sidebar_label: 'Hugging Face',
|
||||
}
|
||||
|
||||
[Hugging Face](https://huggingface.co) is an open source hub for AI/ML models and tools. With over 100,000 machine learning models available, Hugging Face provides a great way to integrate specialized AI & ML tasks into your application.
|
||||
|
||||
Hugging Face exposes an [Inference API](https://huggingface.co/inference-api) you can use to execute AI tasks remotely on Hugging Face servers. This opens the doors to using Hugging Face with languages like TypeScript and can be deployed using [Edge Functions](/docs/guides/functions).
|
||||
|
||||
## AI Tasks
|
||||
|
||||
Below are some of the types of tasks you can perform with Hugging Face:
|
||||
|
||||
### Natural language
|
||||
|
||||
- [Summarization](https://huggingface.co/tasks/summarization)
|
||||
- [Text classification](https://huggingface.co/tasks/text-classification)
|
||||
- [Text generation](https://huggingface.co/tasks/text-generation)
|
||||
- [Translation](https://huggingface.co/tasks/translation)
|
||||
- [Fill in the blank](https://huggingface.co/tasks/fill-mask)
|
||||
|
||||
### Computer Vision
|
||||
|
||||
- [Image to text](https://huggingface.co/tasks/image-to-text)
|
||||
- [Text to image](https://huggingface.co/tasks/text-to-image)
|
||||
- [Image classification](https://huggingface.co/tasks/image-classification)
|
||||
- [Video classification](https://huggingface.co/tasks/video-classification)
|
||||
- [Object detection](https://huggingface.co/tasks/object-detection)
|
||||
- [Image segmentation](https://huggingface.co/tasks/image-segmentation)
|
||||
|
||||
### Audio
|
||||
|
||||
- [Text to speech](https://huggingface.co/tasks/text-to-speech)
|
||||
- [Speech to text](https://huggingface.co/tasks/automatic-speech-recognition)
|
||||
- [Audio classification](https://huggingface.co/tasks/audio-classification)
|
||||
|
||||
See a [full list of tasks](https://huggingface.co/tasks).
|
||||
|
||||
## Access token
|
||||
|
||||
First generate a Hugging Face access token for your app:
|
||||
|
||||
https://huggingface.co/settings/tokens
|
||||
|
||||
Name your token based on the app its being used for and the environment. For example, if you are building an image generation app you might create 2 tokens:
|
||||
|
||||
- "My Image Generator (Dev)"
|
||||
- "My Image Generator (Prod)"
|
||||
|
||||
Since we will be using this token for the inference API, choose the `read` role.
|
||||
|
||||
<Admonition type="info">
|
||||
|
||||
Though it is possible to use the Hugging Face inference API today without an access token, [you may be rate limited](https://huggingface.co/docs/huggingface.js/inference/README#usage).
|
||||
|
||||
To ensure you don't experience any unexpected downtime or errors, we recommend creating an access token.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Edge Functions
|
||||
|
||||
Edge Functions are server-side TypeScript functions that run on-demand. Since Edge Functions run on a server, you can safely give them access to your Hugging Face access token.
|
||||
|
||||
<Admonition type="info">
|
||||
|
||||
You will need the `supabase` CLI [installed](/docs/guides/cli) for the following commands to work.
|
||||
|
||||
</Admonition>
|
||||
|
||||
To create a new Edge Function, navigate to your local project and initialize Supabase if you haven't already:
|
||||
|
||||
```shell
|
||||
supabase init
|
||||
```
|
||||
|
||||
Then create an Edge Function:
|
||||
|
||||
```shell
|
||||
supabase functions new text-to-image
|
||||
```
|
||||
|
||||
Create a file called `.env.local` to store your Hugging Face access token:
|
||||
|
||||
```shell
|
||||
HUGGING_FACE_ACCESS_TOKEN=<your-token-here>
|
||||
```
|
||||
|
||||
Let's modify the Edge Function to import Hugging Face's inference client and perform a `text-to-image` request:
|
||||
|
||||
```ts
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
import { HfInference } from 'https://esm.sh/@huggingface/inference@2.3.2'
|
||||
|
||||
const hf = new HfInference(Deno.env.get('HUGGING_FACE_ACCESS_TOKEN'))
|
||||
|
||||
serve(async (req) => {
|
||||
const { prompt } = await req.json()
|
||||
|
||||
const image = await hf.textToImage(
|
||||
{
|
||||
inputs: prompt,
|
||||
model: 'stabilityai/stable-diffusion-2',
|
||||
},
|
||||
{
|
||||
use_cache: false,
|
||||
}
|
||||
)
|
||||
|
||||
return new Response(image)
|
||||
})
|
||||
```
|
||||
|
||||
1. This function creates a new instance of `HfInference` using the `HUGGING_FACE_ACCESS_TOKEN` environment variable.
|
||||
|
||||
1. It expects a POST request that includes a JSON request body. The JSON body should include a parameter called `prompt` that represents the text-to-image prompt that we will pass to Hugging Face's inference API.
|
||||
|
||||
1. Next we call `textToImage()`, passing in the user's prompt along with the model that we would like to use for the image generation. Today Hugging Face recommends `stabilityai/stable-diffusion-2`, but you can change this to any other text-to-image model. You can see a list of which models are supported for each task by navigating to their [models page](https://huggingface.co/models?pipeline_tag=text-to-image) and filtering by task.
|
||||
|
||||
1. We set `use_cache` to `false` so that repeat queries with the same prompt will produce new images. If the task and model you are using is deterministic (will always produce the same result based on the same input), consider setting `use_cache` to `true` for faster responses.
|
||||
|
||||
1. The `image` result returned from the API will be a `Blob`. We can pass the `Blob` directly into a `new Response()` which will automatically set the content type and body of the response from the `image`.
|
||||
|
||||
Finally let's serve the Edge Function locally to test it:
|
||||
|
||||
```shell
|
||||
supabase functions serve --env-file .env.local --no-verify-jwt
|
||||
```
|
||||
|
||||
Remember to pass in the `.env.local` file using the `--env-file` parameter so that the Edge Function can access the `HUGGING_FACE_ACCESS_TOKEN`.
|
||||
|
||||
<Admonition type="info">
|
||||
|
||||
For demo purposes we set `--no-verify-jwt` to make it easy to test the Edge Function without passing in a JWT token. In a real application you will need to pass the JWT as a `Bearer` token in the `Authorization` header.
|
||||
|
||||
</Admonition>
|
||||
|
||||
At this point, you can make an API request to your Edge Function using your preferred frontend framework (Next.js, React, Expo, etc). We can also test from the terminal using `curl`:
|
||||
|
||||
```shell
|
||||
curl --output result.jpg --location --request POST 'http://localhost:54321/functions/v1/text-to-image' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"query":"Llama wearing sunglasses"}'
|
||||
```
|
||||
|
||||
In this example, your generated image will save to `result.jpg`:
|
||||
|
||||
<img
|
||||
src="/docs/img/ai/hugging-face/llama-sunglasses-example.png"
|
||||
alt="Llama wearing sunglasses example"
|
||||
width="400"
|
||||
height="400"
|
||||
/>
|
||||
|
||||
## Next steps
|
||||
|
||||
You can now create an Edge Function that invokes a Hugging Face task using your model of choice.
|
||||
|
||||
Try running some other [AI tasks](#ai-tasks).
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [Hugging Face site](https://huggingface.co/).
|
||||
- Official [Hugging Face JS docs](https://huggingface.co/docs/huggingface.js).
|
||||
- [Generate image captions](/docs/guides/ai/examples/huggingface-image-captioning) using Hugging Face.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,66 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-integration-llamaindex',
|
||||
title:
|
||||
'Learn how to integrate Supabase with LlamaIndex, a data framework for your LLM applications.',
|
||||
subtitle:
|
||||
'Learn how to integrate Supabase with LlamaIndex, a data framework for your LLM applications.',
|
||||
breadcrumb: 'AI Integrations',
|
||||
}
|
||||
|
||||
This guide will walk you through a basic example using the LlamaIndex [SupabaseVectorStore](https://github.com/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb).
|
||||
|
||||
<DatabaseSetup />
|
||||
|
||||
## Launching a notebook
|
||||
|
||||
Launch our [LlamaIndex](https://github.com/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb) notebook in Colab:
|
||||
|
||||
<a
|
||||
className="w-64"
|
||||
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb"
|
||||
>
|
||||
<img src="/docs/img/ai/colab-badge.svg" />
|
||||
</a>
|
||||
|
||||
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
|
||||
|
||||
## Fill in your OpenAI credentials
|
||||
|
||||
Inside the Notebook, add your `OPENAI_API_KEY` key. Find the cell which contains this code:
|
||||
|
||||
```py
|
||||
import os
|
||||
os.environ['OPENAI_API_KEY'] = "[your_openai_api_key]"
|
||||
```
|
||||
|
||||
## Connecting to your database
|
||||
|
||||
Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:
|
||||
|
||||
```python
|
||||
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
```
|
||||
|
||||
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
|
||||
|
||||
## Stepping through the notebook
|
||||
|
||||
Now all that's left is to step through the notebook. You can do this by clicking the "execute" button (`ctrl+enter`) at the top left of each code cell. The notebook guides you through the process of creating a collection, adding data to it, and querying it.
|
||||
|
||||
You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
|
||||
|
||||

|
||||
|
||||
## Resources
|
||||
|
||||
- Visit the LlamaIndex + SupabaseVectorStore [docs](https://gpt-index.readthedocs.io/en/latest/examples/vector_stores/SupabaseVectorIndexDemo.html)
|
||||
- Visit the official LlamaIndex [repo](https://github.com/jerryjliu/llama_index/)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,215 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-lang-chain',
|
||||
title: 'LangChain',
|
||||
description:
|
||||
'Learn how to integrate Supabase with LangChain, a popular framework for composing AI, Vectors, and embeddings',
|
||||
sidebar_label: 'LangChain',
|
||||
}
|
||||
|
||||
[LangChain](langchain.com) is a popular framework for working with AI, Vectors, and embeddings. LangChain supports using Supabase as a [vector store](https://js.langchain.com/docs/modules/indexes/vector_stores/integrations/supabase), using the `pgvector` extension.
|
||||
|
||||
## Initializing your database
|
||||
|
||||
Prepare you database with the relevant tables:
|
||||
|
||||
```sql
|
||||
-- Enable the pgvector extension to work with embedding vectors
|
||||
create extension vector;
|
||||
|
||||
-- Create a table to store your documents
|
||||
create table documents (
|
||||
id bigserial primary key,
|
||||
content text, -- corresponds to Document.pageContent
|
||||
metadata jsonb, -- corresponds to Document.metadata
|
||||
embedding vector(1536) -- 1536 works for OpenAI embeddings, change if needed
|
||||
);
|
||||
|
||||
-- Create a function to search for documents
|
||||
create function match_documents (
|
||||
query_embedding vector(1536),
|
||||
match_count int default null,
|
||||
filter jsonb DEFAULT '{}'
|
||||
) returns table (
|
||||
id bigint,
|
||||
content text,
|
||||
metadata jsonb,
|
||||
similarity float
|
||||
)
|
||||
language plpgsql
|
||||
as $$
|
||||
#variable_conflict use_column
|
||||
begin
|
||||
return query
|
||||
select
|
||||
id,
|
||||
content,
|
||||
metadata,
|
||||
1 - (documents.embedding <=> query_embedding) as similarity
|
||||
from documents
|
||||
where metadata @> filter
|
||||
order by documents.embedding <=> query_embedding
|
||||
limit match_count;
|
||||
end;
|
||||
$$;
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
You can now search your documents using any Node.js application. This is intended to be run on a secure server route.
|
||||
|
||||
```js
|
||||
import { SupabaseVectorStore } from 'langchain/vectorstores/supabase'
|
||||
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
if (!supabaseKey) throw new Error(`Expected SUPABASE_SERVICE_ROLE_KEY`)
|
||||
|
||||
const url = process.env.SUPABASE_URL
|
||||
if (!url) throw new Error(`Expected env var SUPABASE_URL`)
|
||||
|
||||
export const run = async () => {
|
||||
const client = createClient(url, supabaseKey)
|
||||
|
||||
const vectorStore = await SupabaseVectorStore.fromTexts(
|
||||
['Hello world', 'Bye bye', "What's this?"],
|
||||
[{ id: 2 }, { id: 1 }, { id: 3 }],
|
||||
new OpenAIEmbeddings(),
|
||||
{
|
||||
client,
|
||||
tableName: 'documents',
|
||||
queryName: 'match_documents',
|
||||
}
|
||||
)
|
||||
|
||||
const resultOne = await vectorStore.similaritySearch('Hello world', 1)
|
||||
|
||||
console.log(resultOne)
|
||||
}
|
||||
```
|
||||
|
||||
### Simple Metadata Filtering
|
||||
|
||||
Given the above `match_documents` Postgres function, you can also pass a filter parameter to only return documents with a specific metadata field value. This filter parameter is a JSON object, and the `match_documents` function will use the Postgres JSONB Containment operator `@>` to filter documents by the metadata field values you specify. See details on the [Postgres JSONB Containment operator](https://www.postgresql.org/docs/current/datatype-json.html#JSON-CONTAINMENT) for more information.
|
||||
|
||||
```js
|
||||
import { SupabaseVectorStore } from 'langchain/vectorstores/supabase'
|
||||
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// First, follow set-up instructions above
|
||||
|
||||
const privateKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
if (!privateKey) throw new Error(`Expected env var SUPABASE_SERVICE_ROLE_KEY`)
|
||||
|
||||
const url = process.env.SUPABASE_URL
|
||||
if (!url) throw new Error(`Expected env var SUPABASE_URL`)
|
||||
|
||||
export const run = async () => {
|
||||
const client = createClient(url, privateKey)
|
||||
|
||||
const vectorStore = await SupabaseVectorStore.fromTexts(
|
||||
['Hello world', 'Hello world', 'Hello world'],
|
||||
[{ user_id: 2 }, { user_id: 1 }, { user_id: 3 }],
|
||||
new OpenAIEmbeddings(),
|
||||
{
|
||||
client,
|
||||
tableName: 'documents',
|
||||
queryName: 'match_documents',
|
||||
}
|
||||
)
|
||||
|
||||
const result = await vectorStore.similaritySearch('Hello world', 1, {
|
||||
user_id: 3,
|
||||
})
|
||||
|
||||
console.log(result)
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Metadata Filtering
|
||||
|
||||
You can also use query builder-style filtering ([similar to how the Supabase JavaScript library works](https://supabase.com/docs/reference/javascript/using-filters)) instead of passing an object. Note that since the filter properties will be in the metadata column, you need to use arrow operators (`->` for integer or `->>` for text) as defined in [Postgrest API documentation](https://postgrest.org/en/stable/references/api/tables_views.html?highlight=operators#json-columns) and specify the data type of the property (e.g. the column should look something like `metadata->some_int_value::int`).
|
||||
|
||||
```js
|
||||
import { SupabaseFilterRPCCall, SupabaseVectorStore } from 'langchain/vectorstores/supabase'
|
||||
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// First, follow set-up instructions above
|
||||
|
||||
const privateKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
if (!privateKey) throw new Error(`Expected env var SUPABASE_SERVICE_ROLE_KEY`)
|
||||
|
||||
const url = process.env.SUPABASE_URL
|
||||
if (!url) throw new Error(`Expected env var SUPABASE_URL`)
|
||||
|
||||
export const run = async () => {
|
||||
const client = createClient(url, privateKey)
|
||||
|
||||
const embeddings = new OpenAIEmbeddings()
|
||||
|
||||
const store = new SupabaseVectorStore(embeddings, {
|
||||
client,
|
||||
tableName: 'documents',
|
||||
})
|
||||
|
||||
const docs = [
|
||||
{
|
||||
pageContent:
|
||||
'This is a long text, but it actually means something because vector database does not understand Lorem Ipsum. So I would need to expand upon the notion of quantum fluff, a theorectical concept where subatomic particles coalesce to form transient multidimensional spaces. Yet, this abstraction holds no real-world application or comprehensible meaning, reflecting a cosmic puzzle.',
|
||||
metadata: { b: 1, c: 10, stuff: 'right' },
|
||||
},
|
||||
{
|
||||
pageContent:
|
||||
'This is a long text, but it actually means something because vector database does not understand Lorem Ipsum. So I would need to proceed by discussing the echo of virtual tweets in the binary corridors of the digital universe. Each tweet, like a pixelated canary, hums in an unseen frequency, a fascinatingly perplexing phenomenon that, while conjuring vivid imagery, lacks any concrete implication or real-world relevance, portraying a paradox of multidimensional spaces in the age of cyber folklore.',
|
||||
metadata: { b: 2, c: 9, stuff: 'right' },
|
||||
},
|
||||
{ pageContent: 'hello', metadata: { b: 1, c: 9, stuff: 'right' } },
|
||||
{ pageContent: 'hello', metadata: { b: 1, c: 9, stuff: 'wrong' } },
|
||||
{ pageContent: 'hi', metadata: { b: 2, c: 8, stuff: 'right' } },
|
||||
{ pageContent: 'bye', metadata: { b: 3, c: 7, stuff: 'right' } },
|
||||
{ pageContent: "what's this", metadata: { b: 4, c: 6, stuff: 'right' } },
|
||||
]
|
||||
|
||||
await store.addDocuments(docs)
|
||||
|
||||
const funcFilterA: SupabaseFilterRPCCall = (rpc) =>
|
||||
rpc
|
||||
.filter('metadata->b::int', 'lt', 3)
|
||||
.filter('metadata->c::int', 'gt', 7)
|
||||
.textSearch('content', `'multidimensional' & 'spaces'`, {
|
||||
config: 'english',
|
||||
})
|
||||
|
||||
const resultA = await store.similaritySearch('quantum', 4, funcFilterA)
|
||||
|
||||
const funcFilterB: SupabaseFilterRPCCall = (rpc) =>
|
||||
rpc
|
||||
.filter('metadata->b::int', 'lt', 3)
|
||||
.filter('metadata->c::int', 'gt', 7)
|
||||
.filter('metadata->>stuff', 'eq', 'right')
|
||||
|
||||
const resultB = await store.similaritySearch('hello', 2, funcFilterB)
|
||||
|
||||
console.log(resultA, resultB)
|
||||
}
|
||||
```
|
||||
|
||||
## Hybrid search
|
||||
|
||||
LangChain supports the concept of a hybrid search, which combines Similarity Search with Full Text Search. Read the official docs to get started: [Supabase Hybrid Search](https://js.langchain.com/docs/modules/indexes/retrievers/supabase-hybrid).
|
||||
|
||||
You can install the LangChain Hybrid Search function though our [database.dev package manager](https://database.dev/langchain/hybrid_search).
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [LangChain site](https://langchain.com/).
|
||||
- Official [LangChain docs](https://js.langchain.com/docs/modules/indexes/vector_stores/integrations/supabase).
|
||||
- Supabase [Hybrid Search](https://js.langchain.com/docs/modules/indexes/retrievers/supabase-hybrid).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,156 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-collections',
|
||||
title: 'Managing collections',
|
||||
description: 'Learn how to manage groups of vector records using the vecs Python library',
|
||||
sidebar_label: 'Managing collections',
|
||||
}
|
||||
|
||||
A collection is an group of vector records managed by the `vecs` Python library. Records can be added to or updated in a collection. Collections can be queried at any time, but should be indexed for scalable query performance.
|
||||
|
||||
Supabase provides a [Python client](/docs/guides/ai/vecs-python-client) called `vecs` for managing unstructured vector stores in Postgres. If you come from a data science background, this unstructured data approach will feel familiar. If you are more interested in a structured data approach, see [Vector columns](/docs/guides/ai/vector-columns) or read our guide on [Structured & Unstructured Embeddings](/docs/guides/ai/structured-unstructured-embeddings).
|
||||
|
||||
Under the hood `vecs` will manage the necessary Postgres tables and columns to store and query your collections.
|
||||
|
||||
## API
|
||||
|
||||
Find the full API in the [official API docs](https://supabase.github.io/vecs/api).
|
||||
|
||||
### Connecting
|
||||
|
||||
Before you can interact with vecs, create the client to communicate with Postgres.
|
||||
|
||||
```python
|
||||
import vecs
|
||||
|
||||
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
```
|
||||
|
||||
### Create collection
|
||||
|
||||
You can create a collection to store vectors specifying the collections name and the number of dimensions in the vectors you intend to store.
|
||||
|
||||
```python
|
||||
docs = vx.create_collection(name="docs", dimension=3)
|
||||
```
|
||||
|
||||
If another collection exists with the same name,
|
||||
|
||||
### Get an existing collection
|
||||
|
||||
To access a previously created collection, use `get_collection` to retrieve it by name
|
||||
|
||||
```python
|
||||
docs = vx.get_collection(name="docs")
|
||||
```
|
||||
|
||||
### Upserting vectors
|
||||
|
||||
`vecs` combines the concepts of "insert" and "update" into "upsert". Upserting records adds them to the collection if the `id` is not present, or updates the existing record if the `id` does exist.
|
||||
|
||||
```python
|
||||
# add records to the collection
|
||||
docs.upsert(
|
||||
vectors=[
|
||||
(
|
||||
"vec0", # the vector's identifier
|
||||
[0.1, 0.2, 0.3], # the vector. list or np.array
|
||||
{"year": 1973} # associated metadata
|
||||
),
|
||||
(
|
||||
"vec1",
|
||||
[0.7, 0.8, 0.9],
|
||||
{"year": 2012}
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Create an index
|
||||
|
||||
Collections can be queried immediately after being created.
|
||||
However, for good performance, the collection should be indexed after records have been upserted.
|
||||
|
||||
Indexes should be created **after** the collection has been populated with records. Building an index on an empty collection will result in significantly reduced recall. Once the index has been created you can still upsert new documents into the collection but you should rebuild the index if the size of the collection more than doubles.
|
||||
|
||||
Only one index may exist per-collection. By default, creating an index will replace any existing index.
|
||||
|
||||
To create an index:
|
||||
|
||||
```python
|
||||
##
|
||||
# INSERT RECORDS HERE
|
||||
##
|
||||
|
||||
# index the collection to be queried by cosine distance
|
||||
docs.create_index(measure=vecs.IndexMeasure.cosine_distance)
|
||||
```
|
||||
|
||||
Available options for query `measure` are:
|
||||
|
||||
- `vecs.IndexMeasure.cosine_distance`
|
||||
- `vecs.IndexMeasure.l2_distance`
|
||||
- `vecs.IndexMeasure.max_inner_product`
|
||||
|
||||
which correspond to different methods for comparing query vectors to the vectors in the database.
|
||||
|
||||
If you aren't sure which to use, stick with the default (cosine_distance) by omitting the parameter i.e.: `docs.create_index()`.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
The time required to create an index grows with the number of records and size of vectors. For a few thousand records expect sub-minute a response in under a minute. It may take a few minutes for larger collections.
|
||||
|
||||
</Admonition>
|
||||
|
||||
For an in-depth guide on vector indexes, see [Managing indexes](/docs/guides/ai/managing-indexes).
|
||||
|
||||
### Query
|
||||
|
||||
Be aware that indexes are essential for good performance. If you do not create an index, every query will return a warning that includes the `IndexMeasure` you should index.
|
||||
|
||||
#### Basic
|
||||
|
||||
The simplest form of search is to provide a query vector.
|
||||
|
||||
```python
|
||||
docs.query(
|
||||
query_vector=[0.4,0.5,0.6], # required
|
||||
limit=5, # number of records to return
|
||||
filters={}, # metadata filters
|
||||
measure="cosine_distance", # distance measure to use
|
||||
include_value=False, # should distance measure values be returned?
|
||||
include_metadata=False, # should record metadata be returned?
|
||||
)
|
||||
```
|
||||
|
||||
Which returns a list of vector record `ids`.
|
||||
|
||||
#### Metadata Filtering
|
||||
|
||||
The metadata that is associated with each record can also be filtered during a query.
|
||||
|
||||
As an example, `{"year": {"$eq": 2005}}` filters a `year` metadata key to be equal to 2005
|
||||
|
||||
In context:
|
||||
|
||||
```python
|
||||
docs.query(
|
||||
query_vector=[0.4,0.5,0.6],
|
||||
filters={"year": {"$eq": 2012}}, # metadata filters
|
||||
)
|
||||
```
|
||||
|
||||
For a complete reference, see the [metadata guide](https://supabase.github.io/vecs/concepts_metadata/).
|
||||
|
||||
## Resources
|
||||
|
||||
- Official Vecs Documentation: https://supabase.github.io/vecs/api
|
||||
- Source Code: https://github.com/supabase/vecs
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,95 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-managing-indexes',
|
||||
title: 'Managing indexes',
|
||||
description: 'Understanding vector indexes',
|
||||
sidebar_label: 'Managing indexes',
|
||||
}
|
||||
|
||||
Once your vector table starts to grow, you will likely want to add an index to speed up queries. Without indexes, you'll be performing a sequential scan which can be a resource-intensive operation when you have many records.
|
||||
|
||||
## IVFFlat indexes
|
||||
|
||||
Today `pgvector` indexes use an algorithm called IVFFlat. IVF stands for 'inverted file indexes'. It works by clustering your vectors in order to reduce the similarity search scope. Rather than comparing a vector to every other vector, the vector is only compared against vectors within the same cell cluster (or nearby clusters, depending on your configuration).
|
||||
|
||||
### Inverted lists (cell clusters)
|
||||
|
||||
When you create the index, you choose the number of inverted lists (cell clusters). Increase this number to speed up queries, but at the expense of recall.
|
||||
|
||||
For example, to create an index with 100 lists on a column that uses the cosine operator:
|
||||
|
||||
```sql
|
||||
create index on items using ivfflat (column_name vector_cosine_ops) with (lists = 100);
|
||||
```
|
||||
|
||||
For more info on the different operators, see [Distance operations](#distance-operators).
|
||||
|
||||
For every query, you can set the number of probes (1 by default). The number of probes corresponds to the number of nearby cells to probe for a match. Increase this for better recall at the expense of speed.
|
||||
|
||||
To set the number of probes for the duration of the session run:
|
||||
|
||||
```sql
|
||||
set ivfflat.probes = 10;
|
||||
```
|
||||
|
||||
To set the number of probes only for the current transaction run:
|
||||
|
||||
```sql
|
||||
begin;
|
||||
set local ivfflat.probes = 10;
|
||||
select ...
|
||||
commit;
|
||||
```
|
||||
|
||||
If the number of probes is the same as the number of lists, exact nearest neighbor search will be performed and the planner won't use the index.
|
||||
|
||||
### Approximate nearest neighbor
|
||||
|
||||
One important note with IVF indexes is that nearest neighbor search is approximate, since exact search on high dimensional data can't be indexed efficiently. This means that similarity results will change (slightly) after you add an index (trading recall for speed).
|
||||
|
||||
## Distance operators
|
||||
|
||||
The type of index required depends on the distance operator you are using. `pgvector` includes 3 distance operators:
|
||||
|
||||
| Operator | Description | [**Operator class**](https://www.postgresql.org/docs/current/sql-createopclass.html) |
|
||||
| -------- | ---------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `<->` | Euclidean distance | `vector_l2_ops` |
|
||||
| `<#>` | negative inner product | `vector_ip_ops` |
|
||||
| `<=>` | cosine distance | `vector_cosine_ops` |
|
||||
|
||||
Use the following SQL commands to create an index for the operator(s) used in your queries.
|
||||
|
||||
### Euclidean L2 distance (`vector_l2_ops`)
|
||||
|
||||
```sql
|
||||
create index on items using ivfflat (column_name vector_l2_ops) with (lists = 100);
|
||||
```
|
||||
|
||||
### Inner product (`vector_ip_ops`)
|
||||
|
||||
```sql
|
||||
create index on items using ivfflat (column_name vector_ip_ops) with (lists = 100);
|
||||
```
|
||||
|
||||
### Cosine distance (`vector_cosine_ops`)
|
||||
|
||||
```sql
|
||||
create index on items using ivfflat (column_name vector_cosine_ops) with (lists = 100);
|
||||
```
|
||||
|
||||
Currently vectors with up to 2,000 dimensions can be indexed.
|
||||
|
||||
If you are using the `vecs` Python library, follow the instructions in [Managing collections](/docs/guides/ai/managing-collections#create-an-index) to create indexes.
|
||||
|
||||
## When should you add indexes?
|
||||
|
||||
`pgvector` recommends adding indexes only after the table has sufficient data, so that the internal IVFFlat cell clusters are based on your data's distribution. Anytime the distribution changes significantly, consider recreating indexes.
|
||||
|
||||
## Resources
|
||||
|
||||
Read more about indexing on `pgvector`'s [GitHub page](https://github.com/pgvector/pgvector#indexing).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,156 @@
|
||||
import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
|
||||
import { GetStaticPaths, GetStaticProps } from 'next'
|
||||
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import { relative } from 'path'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
|
||||
import components from '~/components'
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const org = 'supabase'
|
||||
const repo = 'vecs'
|
||||
const branch = 'main'
|
||||
const docsDir = 'docs'
|
||||
const externalSite = 'https://supabase.github.io/vecs'
|
||||
|
||||
// Each external docs page is mapped to a local page
|
||||
const pageMap = [
|
||||
{
|
||||
slug: 'api',
|
||||
meta: {
|
||||
title: 'API',
|
||||
},
|
||||
remoteFile: 'api.md',
|
||||
},
|
||||
{
|
||||
slug: 'collections',
|
||||
meta: {
|
||||
title: 'Collections',
|
||||
},
|
||||
remoteFile: 'concepts_collections.md',
|
||||
},
|
||||
{
|
||||
slug: 'indexes',
|
||||
meta: {
|
||||
title: 'Indexes',
|
||||
},
|
||||
remoteFile: 'concepts_indexes.md',
|
||||
},
|
||||
{
|
||||
slug: 'metadata',
|
||||
meta: {
|
||||
title: 'Metadata',
|
||||
},
|
||||
remoteFile: 'concepts_metadata.md',
|
||||
},
|
||||
]
|
||||
|
||||
interface PythonClientDocsProps {
|
||||
source: MDXRemoteSerializeResult
|
||||
meta: {
|
||||
title: string
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function PythonClientDocs({ source, meta }: PythonClientDocsProps) {
|
||||
return (
|
||||
<Layout meta={meta}>
|
||||
<MDXRemote {...source} components={components} />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch markdown from external repo and transform links
|
||||
*/
|
||||
export const getStaticProps: GetStaticProps<PythonClientDocsProps> = async ({ params }) => {
|
||||
const page = pageMap.find(({ slug }) => slug === params.slug)
|
||||
|
||||
if (!page) {
|
||||
throw new Error(`No page mapping found for slug '${params.slug}'`)
|
||||
}
|
||||
|
||||
const { remoteFile, meta } = page
|
||||
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
|
||||
)
|
||||
|
||||
const source = await response.text()
|
||||
|
||||
const urlTransform: UrlTransformFunction = (url) => {
|
||||
try {
|
||||
const externalSiteUrl = new URL(externalSite)
|
||||
|
||||
const placeholderHostname = 'placeholder'
|
||||
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
|
||||
|
||||
// Don't modify a url with a FQDN or a url that's only a hash
|
||||
if (hostname !== placeholderHostname || pathname === '/') {
|
||||
return url
|
||||
}
|
||||
|
||||
const relativePage = (
|
||||
pathname.endsWith('.md')
|
||||
? pathname.replace(/\.md$/, '')
|
||||
: relative(externalSiteUrl.pathname, pathname)
|
||||
).replace(/^\//, '')
|
||||
|
||||
const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile)
|
||||
|
||||
// If we have a mapping for this page, use the mapped path
|
||||
if (page) {
|
||||
return page.slug + hash
|
||||
}
|
||||
|
||||
// If we don't have this page in our docs, link to original docs
|
||||
return `${externalSite}/${relativePage}${hash}`
|
||||
} catch (err) {
|
||||
console.error('Error transforming markdown URL', err)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
const codeHikeOptions: CodeHikeConfig = {
|
||||
theme: codeHikeTheme,
|
||||
lineNumbers: true,
|
||||
showCopyButton: true,
|
||||
skipLanguages: [],
|
||||
autoImport: false,
|
||||
}
|
||||
|
||||
const mdxSource = await serialize(source, {
|
||||
scope: {
|
||||
chCodeConfig: codeHikeOptions,
|
||||
},
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
remarkMkDocsAdmonition,
|
||||
[removeTitle, meta.title],
|
||||
[remarkCodeHike, codeHikeOptions],
|
||||
],
|
||||
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
|
||||
},
|
||||
})
|
||||
|
||||
return { props: { source: mdxSource, meta } }
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
return {
|
||||
paths: pageMap.map(({ slug }) => ({
|
||||
params: {
|
||||
slug,
|
||||
},
|
||||
})),
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-vecs-python-client',
|
||||
title: 'Face similarity search',
|
||||
subtitle: 'Identify the celebrities you looks most similar to using Supabase Vecs.',
|
||||
breadcrumb: 'AI Quickstarts',
|
||||
}
|
||||
|
||||
This guide will walk you through a ["Face Similarity Search"](https://github.com/supabase/supabase/blob/master/examples/ai/face_similarity.ipynb) example using Colab and Supabase Vecs. You'll identify the celebrities you (or any other person) looks most similar to. You will:
|
||||
|
||||
1. Launch a Postgres database that uses pgvector to store embeddings
|
||||
1. Launch a notebook that connects to your database
|
||||
1. Load the "`ashraq/tmdb-people-image`" celebrity dataset
|
||||
1. Use the `face_recognition` model to create an embedding for every celebrity photo.
|
||||
1. Search for similar faces inside the dataset.
|
||||
|
||||
<DatabaseSetup />
|
||||
|
||||
## Launching a notebook
|
||||
|
||||
Launch our [`semantic_text_deduplication`](https://github.com/supabase/supabase/blob/master/examples/ai/face_similarity.ipynb) notebook in Colab:
|
||||
|
||||
<a
|
||||
className="w-64"
|
||||
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/face_similarity.ipynb"
|
||||
>
|
||||
<img src="/docs/img/ai/colab-badge.svg" />
|
||||
</a>
|
||||
|
||||
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
|
||||
|
||||
## Connecting to your database
|
||||
|
||||
Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:
|
||||
|
||||
```python
|
||||
import vecs
|
||||
|
||||
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
```
|
||||
|
||||
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
|
||||
|
||||
## Stepping through the notebook
|
||||
|
||||
Now all that's left is to step through the notebook. You can do this by clicking the "execute" button (`ctrl+enter`) at the top left of each code cell. The notebook guides you through the process of creating a collection, adding data to it, and querying it.
|
||||
|
||||
You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
|
||||
|
||||

|
||||
|
||||
## Next steps
|
||||
|
||||
You can now start building your own applications with Vecs. Check our [examples](/docs/guides/ai#examples) for ideas.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,62 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-vecs-python-client',
|
||||
title: 'Creating and managing collections',
|
||||
subtitle: 'Connecting to your database with Colab.',
|
||||
breadcrumb: 'AI Quickstarts',
|
||||
}
|
||||
|
||||
This guide will walk you through a basic ["Hello World"](https://github.com/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb) example using Colab and Supabase Vecs. You'll learn how to:
|
||||
|
||||
1. Launch a Postgres database that uses pgvector to store embeddings
|
||||
1. Launch a notebook that connects to your database
|
||||
1. Create a vector collection
|
||||
1. Add data to the collection
|
||||
1. Query the collection
|
||||
|
||||
<DatabaseSetup />
|
||||
|
||||
## Launching a notebook
|
||||
|
||||
Launch our [`vector_hello_world`](https://github.com/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb) notebook in Colab:
|
||||
|
||||
<a
|
||||
className="w-64"
|
||||
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb"
|
||||
>
|
||||
<img src="/docs/img/ai/colab-badge.svg" />
|
||||
</a>
|
||||
|
||||
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
|
||||
|
||||
## Connecting to your database
|
||||
|
||||
Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:
|
||||
|
||||
```python
|
||||
import vecs
|
||||
|
||||
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
```
|
||||
|
||||
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
|
||||
|
||||
## Stepping through the notebook
|
||||
|
||||
Now all that's left is to step through the notebook. You can do this by clicking the "execute" button (`ctrl+enter`) at the top left of each code cell. The notebook guides you through the process of creating a collection, adding data to it, and querying it.
|
||||
|
||||
You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
|
||||
|
||||

|
||||
|
||||
## Next steps
|
||||
|
||||
You can now start building your own applications with Vecs. Check our [examples](/docs/guides/ai#examples) for ideas.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,65 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import HuggingFaceDeployment from '~/components/MDX/ai/quickstart_hf_deployment.mdx'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-vecs-python-client',
|
||||
title: 'Semantic Text Deduplication',
|
||||
subtitle: 'Finding duplicate movie reviews with Supabase Vecs.',
|
||||
breadcrumb: 'AI Quickstarts',
|
||||
}
|
||||
|
||||
This guide will walk you through a ["Semantic Text Deduplication"](https://github.com/supabase/supabase/blob/master/examples/ai/semantic_text_deduplication.ipynb) example using Colab and Supabase Vecs. You'll learn how to find similar movie reviews using embeddings, and remove any that seem like duplicates. You will:
|
||||
|
||||
1. Launch a Postgres database that uses pgvector to store embeddings
|
||||
1. Launch a notebook that connects to your database
|
||||
1. Load the IMDB dataset
|
||||
1. Use the `sentence-transformers/all-MiniLM-L6-v2` model to create an embedding representing the semantic meaning of each review.
|
||||
1. Search for all duplicates.
|
||||
|
||||
<DatabaseSetup />
|
||||
|
||||
## Launching a notebook
|
||||
|
||||
Launch our [`semantic_text_deduplication`](https://github.com/supabase/supabase/blob/master/examples/ai/semantic_text_deduplication.ipynb) notebook in Colab:
|
||||
|
||||
<a
|
||||
className="w-64"
|
||||
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/semantic_text_deduplication.ipynb"
|
||||
>
|
||||
<img src="/docs/img/ai/colab-badge.svg" />
|
||||
</a>
|
||||
|
||||
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
|
||||
|
||||
## Connecting to your database
|
||||
|
||||
Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:
|
||||
|
||||
```python
|
||||
import vecs
|
||||
|
||||
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
```
|
||||
|
||||
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
|
||||
|
||||
## Stepping through the notebook
|
||||
|
||||
Now all that's left is to step through the notebook. You can do this by clicking the "execute" button (`ctrl+enter`) at the top left of each code cell. The notebook guides you through the process of creating a collection, adding data to it, and querying it.
|
||||
|
||||
You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
|
||||
|
||||

|
||||
|
||||
<HuggingFaceDeployment />
|
||||
|
||||
## Next steps
|
||||
|
||||
You can now start building your own applications with Vecs. Check our [examples](/docs/guides/ai#examples) for ideas.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,114 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'structured-unstructured-embeddings',
|
||||
title: 'Structured and Unstructured',
|
||||
description:
|
||||
'Supabase is flexible enough to associate structured and unstructured metadata with embeddings.',
|
||||
subtitle:
|
||||
'Supabase is flexible enough to associate structured and unstructured metadata with embeddings.',
|
||||
sidebar_label: 'Structured and unstructured embeddings',
|
||||
}
|
||||
|
||||
Most vector stores treat metadata associated with embeddings like NoSQL, unstructured data. Supabase is flexible enough to store unstructured and structured metadata.
|
||||
|
||||
## Structured
|
||||
|
||||
```sql
|
||||
create table docs (
|
||||
id uuid primary key,
|
||||
embedding vector(3),
|
||||
content text,
|
||||
url string
|
||||
);
|
||||
|
||||
insert into docs
|
||||
(id, content, url, embedding)
|
||||
values
|
||||
('79409372-7556-4ccc-ab8f-5786a6cfa4f7', array[0.1, 0.2, 0.3], 'Hello world', '/hello-world');
|
||||
```
|
||||
|
||||
Notice that we've associated two pieces of metadata, `content` and `url`, with the embedding. Those fields can be filtered, constrained, indexed, and generally operated on using the full power of SQL. Structured metadata fits naturally with a traditional Supabase application, and can be managed via database [migrations](/docs/guides/getting-started/local-development#database-migrations).
|
||||
|
||||
## Unstructured
|
||||
|
||||
```sql
|
||||
create table docs (
|
||||
id uuid primary key,
|
||||
embedding vector(3),
|
||||
meta jsonb
|
||||
);
|
||||
|
||||
insert into docs
|
||||
(id, embedding, meta)
|
||||
values
|
||||
(
|
||||
'79409372-7556-4ccc-ab8f-5786a6cfa4f7',
|
||||
array[0.1, 0.2, 0.3],
|
||||
'{"content": "Hello world", "url": "/hello-world"}'
|
||||
);
|
||||
```
|
||||
|
||||
An unstructured approach does not specify the metadata fields that are expected. It stores all metadata in a flexible `json`/`jsonb` column. The tradeoff is that the querying/filtering capabilities of a schemaless data type are less flexible than when each field has a dedicated column. It also pushes the burden of metadata data integrity onto application code, which is more error prone than enforcing constraints in the database.
|
||||
|
||||
The unstructured approach is recommended:
|
||||
|
||||
- for ephemeral/interactive workloads e.g. data science or scientific research
|
||||
- when metadata fields are user-defined or unknown
|
||||
- during rapid prototyping
|
||||
|
||||
Client libraries like python's [vecs](https://github.com/supabase/vecs) use this structure. For example, running:
|
||||
|
||||
```py
|
||||
#!/usr/bin/env python3
|
||||
import vecs
|
||||
|
||||
docs = vx.create_collection(name="docs", dimension=1536)
|
||||
|
||||
docs.upsert(vectors=[
|
||||
('79409372-7556-4ccc-ab8f-5786a6cfa4f7', [100, 200, 300], { url: '/hello-world' })
|
||||
])
|
||||
|
||||
```
|
||||
|
||||
automatically creates the unstructured SQL table during the call to `create_collection`.
|
||||
|
||||
Note that when working with client libraries that emit SQL DDL, like `create table ...`, you should add that SQL to your migrations when moving to production to maintain a single source of truth for your database's schema.
|
||||
|
||||
## Hybrid
|
||||
|
||||
The structured metadata style is recommended when the fields being tracked are known in advance. If you have a combination of known and unknown metadata fields, you can accommodate the unknown fields by adding a `json`/`jsonb` column to the table. In that situation, known fields should continue to use dedicated columns for best query performance and throughput.
|
||||
|
||||
```sql
|
||||
create table docs (
|
||||
id uuid primary key,
|
||||
embedding vector(3),
|
||||
content text,
|
||||
url string,
|
||||
meta jsonb
|
||||
);
|
||||
|
||||
insert into docs
|
||||
(id, embedding, meta)
|
||||
values
|
||||
(
|
||||
'79409372-7556-4ccc-ab8f-5786a6cfa4f7',
|
||||
array[0.1, 0.2, 0.3],
|
||||
'Hello world',
|
||||
'/hello-world',
|
||||
'{"key": "value"}'
|
||||
);
|
||||
```
|
||||
|
||||
## Choosing the right model
|
||||
|
||||
Both approaches create a table where you can store your embeddings and some metadata. You should choose the best approach for your use-case. In summary:
|
||||
|
||||
- Structured metadata is best when fields are known in advance or query patterns are predictable e.g. a production Supabase application
|
||||
- Unstructured metadata is best when fields are unknown/user-defined or when working with data interactively e.g. exploratory research
|
||||
|
||||
Both approaches are valid, and the one you should choose depends on your use-case.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,91 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-vecs-python-client',
|
||||
title: 'Python client',
|
||||
subtitle: 'Manage unstructured vector stores in PostgreSQL.',
|
||||
breadcrumb: 'AI Quickstarts',
|
||||
}
|
||||
|
||||
Supabase provides a Python client called [`vecs`](https://github.com/supabase/vecs) for managing unstructured vector stores. This client provides a set of useful tools for creating and querying collections in PostgreSQL using the [pgvector](/docs/guides/database/extensions/pgvector) extension.
|
||||
|
||||
## Quick start
|
||||
|
||||
Let's see how Vecs works using a local database. Make sure you have the Supabase CLI [installed](/docs/guides/cli#installation) on your machine.
|
||||
|
||||
### Initialize your project
|
||||
|
||||
Start a local Postgres instance in any folder using the `init` and `start` commands. Make sure you have Docker running!
|
||||
|
||||
```bash
|
||||
# Initialize your project
|
||||
supabase init
|
||||
|
||||
# Start Postgres
|
||||
supabase start
|
||||
```
|
||||
|
||||
### Create a collection
|
||||
|
||||
Inside a Python shell, run the following commands to create a new collection called "docs", with 3 dimensions.
|
||||
|
||||
```py
|
||||
import vecs
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client("postgresql://postgres:postgres@localhost:54322/postgres")
|
||||
|
||||
# create a collection of vectors with 3 dimensions
|
||||
docs = vx.create_collection(name="docs", dimension=3)
|
||||
```
|
||||
|
||||
### Add embeddings
|
||||
|
||||
Now we can insert some embeddings into our "docs" collection using the `usert()` command:
|
||||
|
||||
```py
|
||||
import vecs
|
||||
|
||||
# create vector store client
|
||||
docs = vecs.get_collection(name="docs")
|
||||
|
||||
# a collection of vectors with 3 dimensions
|
||||
vectors=[
|
||||
("vec0", [0.1, 0.2, 0.3], {"year": 1973}),
|
||||
("vec1", [0.7, 0.8, 0.9], {"year": 2012})
|
||||
]
|
||||
|
||||
# insert our vectors
|
||||
docs.upsert(vectors=vectors)
|
||||
```
|
||||
|
||||
### Query the collection
|
||||
|
||||
You can now query the collection to retrieve a relevant match:
|
||||
|
||||
```py
|
||||
import vecs
|
||||
|
||||
docs = vecs.get_collection(name="docs")
|
||||
|
||||
# query the collection filtering metadata for "year" = 2012
|
||||
docs.query(
|
||||
query_vector=[0.4,0.5,0.6], # required
|
||||
limit=1, # number of records to return
|
||||
filters={"year": {"$eq": 2012}}, # metadata filters
|
||||
)
|
||||
```
|
||||
|
||||
## Deep Dive
|
||||
|
||||
For a more in-depth guide on `vecs` collections, see [Managing collections](/docs/guides/ai/managing-collections).
|
||||
|
||||
## Resources
|
||||
|
||||
- Official Vecs Documentation: https://supabase.github.io/vecs/api
|
||||
- Source Code: https://github.com/supabase/vecs
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,153 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-vector-columns',
|
||||
title: 'Vector columns',
|
||||
description: 'Learn how to use vectors within your own Postgres tables',
|
||||
sidebar_label: 'Vector columns',
|
||||
}
|
||||
|
||||
Supabase offers a number of different ways to store and query vectors within Postgres. If you prefer to use Python to store and query your vectors using collections, see [Managing collections](/docs/guides/ai/managing-collections). If you want more control over vectors within your own Postgres tables or would like to interact with them using a different language like JavaScript, keep reading.
|
||||
|
||||
Vectors in Supabase are enabled via [pgvector](https://github.com/pgvector/pgvector/), a PostgreSQL extension for storing and querying vectors in Postgres. It can be used to store [embeddings](/docs/guides/ai/concepts#what-are-embeddings).
|
||||
|
||||
## Usage
|
||||
|
||||
### Enable the extension
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "vector" and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Example: enable the "vector" extension.
|
||||
create extension vector
|
||||
with
|
||||
schema extensions;
|
||||
|
||||
-- Example: disable the "vector" extension
|
||||
drop
|
||||
extension if exists vector;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
To disable an extension, call `drop extension`.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Create a table to store vectors
|
||||
|
||||
After enabling the `vector` extension, you will get access to a new data type called `vector`. The size of the vector (indicated in parenthesis) represents the number of dimensions stored in that vector.
|
||||
|
||||
```sql
|
||||
create table documents (
|
||||
id serial primary key,
|
||||
title text not null,
|
||||
body text not null,
|
||||
embedding vector(1536)
|
||||
);
|
||||
```
|
||||
|
||||
In the above SQL snippet, we create a `documents` table with a column called `embedding` (note this is just a regular Postgres column - you can name it whatever you like). We give the `embedding` column a `vector` data type with 1536 dimensions. Change this to the number of dimensions used in your vector application. For example, if you are generating embeddings using OpenAI's `text-embeddings-ada-002` model, you would set this number as 1536 since that model produces 1536 dimensions.
|
||||
|
||||
### Storing a vector / embedding
|
||||
|
||||
In this example we'll generate a vector using the OpenAI API client, then store it in the database using the Supabase JavaScript client.
|
||||
|
||||
```js
|
||||
const title = 'First post!'
|
||||
const body = 'Hello world!'
|
||||
|
||||
// Generate a vector using OpenAI
|
||||
const embeddingResponse = await openai.createEmbedding({
|
||||
model: 'text-embedding-ada-002',
|
||||
input: body,
|
||||
})
|
||||
|
||||
const [{ embedding }] = embeddingResponse.data.data
|
||||
|
||||
// Store the vector in Postgres
|
||||
const { data, error } = await supabase.from('documents').insert({
|
||||
title,
|
||||
body,
|
||||
embedding,
|
||||
})
|
||||
```
|
||||
|
||||
This example uses the JavaScript Supabase client, but you can modify it to work with any [supported language library](/docs#client-libraries).
|
||||
|
||||
### Querying a vector / embedding
|
||||
|
||||
Similarity search is the most common use case for vectors. `pgvector` support 3 new operators for performing similarity search:
|
||||
|
||||
| Operator | Description |
|
||||
| -------- | ---------------------- |
|
||||
| `<->` | Euclidean distance |
|
||||
| `<#>` | negative inner product |
|
||||
| `<=>` | cosine distance |
|
||||
|
||||
Choosing the right operator depends on your needs. If you are searching over OpenAI embeddings, OpenAI recommends using cosine similarity. For more information on how embeddings work and how they relate to each other, see [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings).
|
||||
|
||||
Supabase client libraries like `supabase-js` connect to your Postgres instance via [PostgREST](docs/guides/getting-started/architecture#postgrest-api). PostgREST does not currently support `pgvector` similarity operators, so we'll need to wrap our query in a Postgres function and call it via the `rpc()` method:
|
||||
|
||||
```sql
|
||||
create or replace function match_documents (
|
||||
query_embedding vector(1536),
|
||||
match_threshold float,
|
||||
match_count int
|
||||
)
|
||||
returns table (
|
||||
id bigint,
|
||||
content text,
|
||||
similarity float
|
||||
)
|
||||
language sql stable
|
||||
as $$
|
||||
select
|
||||
documents.id,
|
||||
documents.content,
|
||||
1 - (documents.embedding <=> query_embedding) as similarity
|
||||
from documents
|
||||
where 1 - (documents.embedding <=> query_embedding) > match_threshold
|
||||
order by similarity desc
|
||||
limit match_count;
|
||||
$$;
|
||||
```
|
||||
|
||||
This function takes a `query_embedding` argument and compares it to all other embeddings in the `documents` table. Each comparison returns a similarity score. If the similarity is greater than the `match_threshold` argument, it is returned. The number of rows returned is limited by the `match_count` argument.
|
||||
|
||||
Feel free to modify this method to fit the needs of your application. The `match_threshold` ensures that only documents that have a minimum similarity to the `query_embedding` are returned. Without this, you may end up returning documents that subjectively don't match. This value will vary for each application - you will need to perform your own testing to determine the threshold that makes sense for your app.
|
||||
|
||||
To execute the function from your client library, call `rpc()` with the name of your Postgres function:
|
||||
|
||||
```ts
|
||||
const { data: documents } = await supabaseClient.rpc('match_documents', {
|
||||
query_embedding: embedding, // Pass the embedding you want to compare
|
||||
match_threshold: 0.78, // Choose an appropriate threshold for your data
|
||||
match_count: 10, // Choose the number of matches
|
||||
})
|
||||
```
|
||||
|
||||
In this example `embedding` would be another embedding you wish to compare against your table of pre-generated embedding documents. For example if you were building a search engine, every time the user submits their query you would first generate an embedding on the search query itself (using `openai.createEmbedding()`), then pass it into the above `rpc()` function to match.
|
||||
|
||||
Vectors and embedding can be used for much more than search. Learn more about embeddings at [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings).
|
||||
|
||||
### Indexes
|
||||
|
||||
Once your vector table starts to grow, you will likely want to add an index to speed up queries. See [Managing indexes](/docs/guides/ai/managing-indexes) to learn how vector indexes work and how to create them.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -74,6 +74,17 @@ Supabase provides a Realtime API using [Realtime](https://github.com/supabase/re
|
||||
Realtime leverages PostgreSQL's built-in logical replication. You can manage your Realtime API simply by managing Postgres publications.
|
||||
Go to your project's [Replication section](https://app.supabase.com/project/_/database/replication) to get started.
|
||||
|
||||
## API URL and Keys
|
||||
|
||||
You can find the API URL and Keys in the [Dashboard](https://app.supabase.com/project/_/settings/api).
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -28,12 +28,10 @@ This creates a corresponding route `todos` which can accept `GET`, `POST`, `PATC
|
||||
1. Click **New Column** and create a column with the name `task` and type `text`.
|
||||
1. Click **Save**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
@@ -60,12 +58,10 @@ Every Supabase project has a unique API URL. Your API is secured behind an API g
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
@@ -207,12 +203,10 @@ By default Realtime is disabled on your database. Let's turn on Realtime for the
|
||||
3. Control which database events are sent by toggling **Insert**, **Update**, and **Delete**.
|
||||
4. Control which tables broadcast changes by selecting **Source** and toggling each table.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-realtime.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
|
||||
@@ -44,12 +44,10 @@ We'll create a database table called `todos` for storing tasks. This creates a c
|
||||
</TabPanel>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
|
||||
@@ -12,12 +12,10 @@ Supabase generates documentation in the [Dashboard](https://app.supabase.com) wh
|
||||
2. Select any table under **Tables and Views** in the sidebar.
|
||||
3. Switch between the JavaScript and the cURL docs using the tabs.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-docs.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
|
||||
|
||||
## Official Libraries
|
||||
|
||||
| `Language` | `Source Code` | `Documentation` |
|
||||
| --------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| Javascript/Typescript | [supabase-js](https://github.com/supabase/supabase-js) | [Docs](https://supabase.com/docs/reference/javascript/introduction) |
|
||||
| Dart/Flutter | [supabase-dart](https://github.com/supabase/supabase-dart) | [Docs](https://supabase.com/docs/reference/dart/introduction) |
|
||||
| `Language` | `Source Code` | `Documentation` |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| Javascript/Typescript | [supabase-js](https://github.com/supabase/supabase-js) | [Docs](https://supabase.com/docs/reference/javascript/introduction) |
|
||||
| Dart/Flutter | [supabase-flutter](https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_flutter) | [Docs](https://supabase.com/docs/reference/dart/introduction) |
|
||||
|
||||
## Community Libraries
|
||||
|
||||
@@ -22,10 +22,10 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
|
||||
| ----------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| C# | [supabase-csharp](https://github.com/supabase-community/supabase-csharp) | [Docs](https://supabase.com/docs/reference/csharp/introduction) |
|
||||
| Go | [supabase-go](https://github.com/supabase-community/supabase-go) | |
|
||||
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | |
|
||||
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | [Docs](https://supabase.com/docs/reference/kotlin/introduction) |
|
||||
| Python | [supabase-py](https://github.com/supabase-community/supabase-py) | [Docs](https://supabase.com/docs/reference/python/initializing) |
|
||||
| Ruby | [supabase-rb](https://github.com/supabase-community/supabase-rb) | |
|
||||
| Swift | [supabase-swift](https://github.com/supabase-community/supabase-swift) | |
|
||||
| Swift | [supabase-swift](https://github.com/supabase-community/supabase-swift) | [Docs](https://supabase.com/docs/reference/swift/introduction) |
|
||||
| Godot Engine (GDScript) | [supabase-gdscript](https://github.com/supabase-community/godot-engine.supabase) | |
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -37,7 +37,7 @@ npx supabase login
|
||||
Generate types for your project to produce the `types/supabase.ts` file:
|
||||
|
||||
```bash
|
||||
npx supabase gen types typescript --project-id "$PROJECT_ID" --schema public > types/supabase.ts
|
||||
npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts
|
||||
```
|
||||
|
||||
After you have generated your types, you can use them in `src/index.ts`
|
||||
@@ -65,7 +65,7 @@ One way to keep your type definitions in sync with your database is to set up a
|
||||
Add the script above to your `package.json` to run it using `npm run update-types`
|
||||
|
||||
```json
|
||||
"update-types": "npx supabase gen types typescript --project-id \"$PROJECT_ID\" > types/supabase.ts"
|
||||
"update-types": "npx supabase gen types typescript --project-id \"$PROJECT_REF\" > types/supabase.ts"
|
||||
```
|
||||
|
||||
Create a file `.github/workflows/update-types.yml` with the following snippet to define the action along with the environment variables. This script will commit new type changes to your repo every night.
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
PROJECT_ID: <your-project-id>
|
||||
PROJECT_REF: <your-project-id>
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
|
||||
@@ -123,12 +123,10 @@ Get started with our [Row Level Security Guides](/docs/guides/auth/row-level-sec
|
||||
|
||||
Authentication only gets you so far. When you need granular authorization rules, nothing beats PostgreSQL's [Row Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html). Supabase makes it simple to turn RLS on and off.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/rls-zoom2.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
@@ -136,12 +134,10 @@ Authentication only gets you so far. When you need granular authorization rules,
|
||||
|
||||
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are PostgreSQL's rule engine. They are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/policies-zoom2.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
@@ -189,12 +185,10 @@ Supabase provides multiple endpoints to authenticate and manage your users:
|
||||
|
||||
When users sign up, Supabase assigns them a unique ID. You can reference this ID anywhere in your database. For example, you might create a `profiles` table referencing `id` in the `auth.users` table using a `user_id` field.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/auth-zoom2.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ const [captchaToken, setCaptchaToken] = useState()
|
||||
|
||||
Now lets add the HCaptcha component to the JSX section of our code
|
||||
|
||||
```html
|
||||
```jsx
|
||||
<HCaptcha />
|
||||
```
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ Future<void> signOut() async {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
|
||||
|
||||
@@ -19,6 +19,14 @@ A collection of framework-specific Auth utilities for working with Supabase.
|
||||
description={'A pre-built React component for authenticating users.'}
|
||||
/>
|
||||
</div>
|
||||
{/* Flutter Auth UI */}
|
||||
<div className="col-span-6">
|
||||
<ButtonCard
|
||||
to={'/guides/auth/auth-helpers/flutter-auth-ui'}
|
||||
title={'Flutter Auth UI'}
|
||||
description={'Pre-built Flutter widgets for authenticating users.'}
|
||||
/>
|
||||
</div>
|
||||
{/* Next.js */}
|
||||
<div className="col-span-6">
|
||||
<ButtonCard
|
||||
|
||||
@@ -9,13 +9,8 @@ export const meta = {
|
||||
Auth UI is a pre-built React component for authenticating users.
|
||||
It supports custom themes and extensible styles to match your brand and aesthetic.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="https://supabase.com/images/blog/lw5-one-more/auth-ui-demo.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source src="https://supabase.com/images/blog/lw5-one-more/auth-ui-demo.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
## Set up Auth UI
|
||||
@@ -30,7 +25,7 @@ npm install @supabase/supabase-js @supabase/auth-ui-react @supabase/auth-ui-shar
|
||||
|
||||
Pass `supabaseClient` from `@supabase/supabase-js` as a prop to the component.
|
||||
|
||||
```js title=/src/index.js
|
||||
```js /src/index.js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
|
||||
@@ -43,7 +38,7 @@ This renders the Auth component without any styling.
|
||||
We recommend using one of the predefined themes to style the UI.
|
||||
Import the theme you want to use and pass it to the `appearance.theme` prop.
|
||||
|
||||
```js lines=4,16 title=/src/index.js
|
||||
```js mark=4,16 /src/index.js
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
import {
|
||||
// Import predefined theme
|
||||
@@ -68,7 +63,7 @@ const App = () => (
|
||||
|
||||
The Auth component also supports login with [official social providers](../../auth#providers).
|
||||
|
||||
```js lines=13 title=/src/index.js
|
||||
```js mark=11 /src/index.js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
import { ThemeSupa } from '@supabase/auth-ui-shared'
|
||||
@@ -84,6 +79,23 @@ const App = () => (
|
||||
)
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
Options are available via `queryParams`:
|
||||
|
||||
```jsx
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
providers={['google']}
|
||||
queryParams={{
|
||||
access_type: 'offline',
|
||||
prompt: 'consent',
|
||||
hd: 'domain.com',
|
||||
}}
|
||||
onlyThirdPartyProviders={true}
|
||||
/>
|
||||
```
|
||||
|
||||
### Supported Views
|
||||
|
||||
The Auth component is currently shipped with the following views:
|
||||
@@ -111,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'
|
||||
@@ -140,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'
|
||||
@@ -166,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'
|
||||
@@ -198,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'
|
||||
|
||||
@@ -248,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'
|
||||
|
||||
@@ -276,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'
|
||||
|
||||
@@ -300,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'
|
||||
|
||||
@@ -309,7 +321,6 @@ const supabase = createClient('<INSERT PROJECT URL>', '<INSERT PROJECT ANON API
|
||||
const App = () => (
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
//highlight-start
|
||||
localization={{
|
||||
variables: {
|
||||
sign_in: {
|
||||
@@ -318,7 +329,6 @@ const App = () => (
|
||||
},
|
||||
},
|
||||
}}
|
||||
//highlight-end
|
||||
/>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'flutter-auth-ui',
|
||||
title: 'Flutter Auth UI',
|
||||
description: 'Prebuilt, customizable Flutter widgets for authenticating users.',
|
||||
}
|
||||
|
||||
Flutter Auth UI is a Flutter package containing pre-built widgets for authenticating users.
|
||||
It is unstyled and can match your brand and aesthetic.
|
||||
|
||||
<image src="https://raw.githubusercontent.com/supabase-community/flutter-auth-ui/main/assets/supabase_auth_ui.png" />
|
||||
|
||||
## Add Flutter Auth UI
|
||||
|
||||
Add the latest version of the package [supabase-auth-ui](https://pub.dev/packages/supabase_auth_ui) to pubspec.yaml:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
supabase_auth_ui: ^0.1.0+2
|
||||
```
|
||||
|
||||
### Initialize the Flutter Auth Package
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
|
||||
|
||||
void main() async {
|
||||
await Supabase.initialize(
|
||||
url: dotenv.get('SUPABASE_URL'),
|
||||
anonKey: dotenv.get('SUPABASE_ANON_KEY'),
|
||||
);
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Email Auth
|
||||
|
||||
Use a SupaEmailAuth widget to create an email and password signin and signup form. It also contains a button to toggle to display a forgot password form.
|
||||
|
||||
You can pass metadataFields to add additional fields to the form to pass as metadata to Supabase.
|
||||
|
||||
```dart
|
||||
SupaEmailAuth(
|
||||
redirectTo: kIsWeb ? null : 'io.mydomain.myapp://callback',
|
||||
onSignInComplete: (response) {},
|
||||
onSignUpComplete: (response) {},
|
||||
metadataFields: [
|
||||
MetaDataField(
|
||||
prefixIcon: const Icon(Icons.person),
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please enter something';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Magic Link Auth
|
||||
|
||||
Use SupaMagicAuth widget to create a magic link signIn form.
|
||||
|
||||
```dart
|
||||
SupaMagicAuth(
|
||||
redirectUrl: kIsWeb ? null : 'io.mydomain.myapp://callback',
|
||||
onSuccess: (Session response) {},
|
||||
onError: (error) {},
|
||||
)
|
||||
```
|
||||
|
||||
### Reset password
|
||||
|
||||
Use SupaResetPassword to create a password reset form.
|
||||
|
||||
```dart
|
||||
SupaResetPassword(
|
||||
accessToken: supabase.auth.currentSession?.accessToken,
|
||||
onSuccess: (UserResponse response) {},
|
||||
onError: (error) {},
|
||||
)
|
||||
```
|
||||
|
||||
### Phone Auth
|
||||
|
||||
Use SupaPhoneAuth to create a phone authentication form.
|
||||
|
||||
```dart
|
||||
SupaPhoneAuth(
|
||||
authAction: SupaAuthAction.signUp,
|
||||
onSuccess: (AuthResponse response) {},
|
||||
),
|
||||
```
|
||||
|
||||
### Social Auth
|
||||
|
||||
The package supports login with [official social providers](../../auth#providers).
|
||||
|
||||
Use SupaSocialsAuth to create list of social login buttons.
|
||||
|
||||
```dart
|
||||
SupaSocialsAuth(
|
||||
socialProviders: [
|
||||
SocialProviders.apple,
|
||||
SocialProviders.google,
|
||||
],
|
||||
colored: true,
|
||||
redirectUrl: kIsWeb
|
||||
? null
|
||||
: 'io.mydomain.myapp://callback',
|
||||
onSuccess: (Session response) {},
|
||||
onError: (error) {},
|
||||
)
|
||||
```
|
||||
|
||||
### Theming
|
||||
|
||||
This package uses plain Flutter components allowing you to control the appearance of the components using your own theme.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,935 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'nextjs-pages',
|
||||
title: 'Supabase Auth with Next.js Pages Directory',
|
||||
description:
|
||||
'Authentication helpers for Next.js API routes, middleware, and SSR in the Pages Directory.',
|
||||
sidebar_label: 'Next.js (pages)',
|
||||
}
|
||||
|
||||
This submodule provides convenience helpers for implementing user authentication in Next.js applications using the pages directory.
|
||||
|
||||
> Note: As of [Next.js 13.4](https://nextjs.org/blog/next-13-4), the App Router has reached stable status. This is now the recommended path for new Next.js app. Check out our guide on using [Auth Helpers with the Next.js App Directory](/docs/guides/auth/auth-helpers/nextjs).
|
||||
|
||||
## Install the Next.js helper library
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="npm"
|
||||
>
|
||||
<TabPanel id="npm" label="npm">
|
||||
|
||||
```sh
|
||||
npm install @supabase/auth-helpers-nextjs
|
||||
```
|
||||
|
||||
This library supports the following tooling versions:
|
||||
|
||||
- Node.js: `^10.13.0 || >=12.0.0`
|
||||
- Next.js: `>=10`
|
||||
|
||||
Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks.
|
||||
|
||||
```sh
|
||||
npm install @supabase/auth-helpers-react
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="yarn" label="Yarn">
|
||||
|
||||
```sh
|
||||
yarn add @supabase/auth-helpers-nextjs
|
||||
```
|
||||
|
||||
This library supports the following tooling versions:
|
||||
|
||||
- Node.js: `^10.13.0 || >=12.0.0`
|
||||
- Next.js: `>=10`
|
||||
|
||||
Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks.
|
||||
|
||||
```sh
|
||||
yarn add @supabase/auth-helpers-react
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Set up environment variables
|
||||
|
||||
Retrieve your project URL and anon key in your project's [API settings](https://app.supabase.com/project/_/settings/api) in the Dashboard to set up the following environment variables. For local development you can set them in a `.env.local` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/nextjs/.env.local.example).
|
||||
|
||||
```bash .env.local
|
||||
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
|
||||
```
|
||||
|
||||
## Basic Setup
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Wrap your `pages/_app.js` component with the `SessionContextProvider` component:
|
||||
|
||||
```jsx pages/_app.js
|
||||
import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider } from '@supabase/auth-helpers-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
// Create a new supabase browser client on every first render.
|
||||
const [supabaseClient] = useState(() => createPagesBrowserClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component:
|
||||
|
||||
```tsx mark=2,8 pages/_app.tsx
|
||||
import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
function MyApp({
|
||||
Component,
|
||||
pageProps,
|
||||
}: AppProps<{
|
||||
initialSession: Session
|
||||
}>) {
|
||||
// Create a new supabase browser client on every first render.
|
||||
const [supabaseClient] = useState(() => createPagesBrowserClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
You can now determine if a user is authenticated by checking that the `user` object returned by the `useUser()` hook is defined.
|
||||
|
||||
### Code Exchange API Route
|
||||
|
||||
The `Code Exchange` API route is required for the [server-side auth flow](https://supabase.com/docs/guides/auth/server-side-rendering) implemented by the Next.js Auth Helpers. It exchanges an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Create a new file at `pages/api/auth/callback.js` and populate with the following:
|
||||
|
||||
```jsx pages/api/auth/callback.js
|
||||
import { NextApiHandler } from 'next'
|
||||
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
const handler = async (req, res) => {
|
||||
const { code } = req.query
|
||||
|
||||
if (code) {
|
||||
const supabase = createPagesServerClient({ req, res })
|
||||
await supabase.auth.exchangeCodeForSession(String(code))
|
||||
}
|
||||
|
||||
res.redirect('/')
|
||||
}
|
||||
|
||||
export default handler
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create a new file at `pages/api/auth/callback.ts` and populate with the following:
|
||||
|
||||
```tsx pages/api/auth/callback.ts
|
||||
import { NextApiHandler } from 'next'
|
||||
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
const handler: NextApiHandler = async (req, res) => {
|
||||
const { code } = req.query
|
||||
|
||||
if (code) {
|
||||
const supabase = createPagesServerClient({ req, res })
|
||||
await supabase.auth.exchangeCodeForSession(String(code))
|
||||
}
|
||||
|
||||
res.redirect('/')
|
||||
}
|
||||
|
||||
export default handler
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Usage with TypeScript
|
||||
|
||||
You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion:
|
||||
|
||||
### Browser client
|
||||
|
||||
Creating a new supabase client object:
|
||||
|
||||
```tsx
|
||||
import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { Database } from '../database.types'
|
||||
|
||||
const supabaseClient = createPagesBrowserClient<Database>()
|
||||
```
|
||||
|
||||
Retrieving a supabase client object from the SessionContext:
|
||||
|
||||
```tsx
|
||||
import { useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import { Database } from '../database.types'
|
||||
|
||||
const supabaseClient = useSupabaseClient<Database>()
|
||||
```
|
||||
|
||||
### Server client
|
||||
|
||||
```tsx
|
||||
// Creating a new supabase server client object (e.g. in API route):
|
||||
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type { Database } from 'types_db'
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const supabaseServerClient = createPagesServerClient<Database>({
|
||||
req,
|
||||
res,
|
||||
})
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabaseServerClient.auth.getUser()
|
||||
|
||||
res.status(200).json({ name: user?.name ?? '' })
|
||||
}
|
||||
```
|
||||
|
||||
## Client-side data fetching with RLS
|
||||
|
||||
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to use the `supabaseClient` from the `useSupabaseClient` hook and only run your query once the user is defined client-side in the `useUser()` hook:
|
||||
|
||||
```jsx mark=10:17
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
import { ThemeSupa } from '@supabase/auth-ui-shared'
|
||||
import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const LoginPage = () => {
|
||||
const supabaseClient = useSupabaseClient()
|
||||
const user = useUser()
|
||||
const [data, setData] = useState()
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
const { data } = await supabaseClient.from('test').select('*')
|
||||
setData(data)
|
||||
}
|
||||
// Only run query once user is logged in.
|
||||
if (user) loadData()
|
||||
}, [user])
|
||||
|
||||
if (!user)
|
||||
return (
|
||||
<Auth
|
||||
redirectTo="http://localhost:3000/"
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
supabaseClient={supabaseClient}
|
||||
providers={['google', 'github']}
|
||||
socialLayout="horizontal"
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => supabaseClient.auth.signOut()}>Sign out</button>
|
||||
<p>user:</p>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
<p>client-side data fetching with RLS</p>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
||||
```
|
||||
|
||||
## Server-side rendering (SSR)
|
||||
|
||||
Create a server supabase client to retrieve the logged in user's session:
|
||||
|
||||
```jsx pages/profile.js
|
||||
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
export default function Profile({ user }) {
|
||||
return <div>Hello {user.name}</div>
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (ctx) => {
|
||||
// Create authenticated Supabase Client
|
||||
const supabase = createPagesServerClient(ctx)
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
if (!session)
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/',
|
||||
permanent: false,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialSession: session,
|
||||
user: session.user,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Server-side data fetching with RLS
|
||||
|
||||
You can use the server supabase client to run [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) authenticated queries server-side:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx
|
||||
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
export default function ProtectedPage({ user, data }) {
|
||||
return (
|
||||
<>
|
||||
<div>Protected content for {user.email}</div>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (ctx) => {
|
||||
// Create authenticated Supabase Client
|
||||
const supabase = createPagesServerClient(ctx)
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
if (!session)
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/',
|
||||
permanent: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Run queries with RLS on the server
|
||||
const { data } = await supabase.from('users').select('*')
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialSession: session,
|
||||
user: session.user,
|
||||
data: data ?? [],
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx
|
||||
import { User, createPagesServerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { GetServerSidePropsContext } from 'next'
|
||||
|
||||
export default function ProtectedPage({ user, data }: { user: User; data: any }) {
|
||||
return (
|
||||
<>
|
||||
<div>Protected content for {user.email}</div>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
// Create authenticated Supabase Client
|
||||
const supabase = createPagesServerClient(ctx)
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
if (!session)
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/',
|
||||
permanent: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Run queries with RLS on the server
|
||||
const { data } = await supabase.from('users').select('*')
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialSession: session,
|
||||
user: session.user,
|
||||
data: data ?? [],
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Server-side data fetching to OAuth APIs using `provider token` {`#oauth-provider-token`}
|
||||
|
||||
When using third-party auth providers, sessions are initiated with an additional `provider_token` field which is persisted in the auth cookie and can be accessed within the session object. The `provider_token` can be used to make API requests to the OAuth provider's API endpoints on behalf of the logged-in user.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx
|
||||
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
export default function ProtectedPage({ user, allRepos }) {
|
||||
return (
|
||||
<>
|
||||
<div>Protected content for {user.email}</div>
|
||||
<p>Data fetched with provider token:</p>
|
||||
<pre>{JSON.stringify(allRepos, null, 2)}</pre>
|
||||
<p>user:</p>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (ctx) => {
|
||||
// Create authenticated Supabase Client
|
||||
const supabase = createPagesServerClient(ctx)
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
if (!session)
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/',
|
||||
permanent: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Retrieve provider_token & logged in user's third-party id from metadata
|
||||
const { provider_token, user } = session
|
||||
const userId = user.user_metadata.user_name
|
||||
|
||||
const allRepos = await (
|
||||
await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `token ${provider_token}`,
|
||||
},
|
||||
})
|
||||
).json()
|
||||
|
||||
return { props: { user, allRepos } }
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx
|
||||
import { User, createPagesServerClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { GetServerSidePropsContext } from 'next'
|
||||
|
||||
export default function ProtectedPage({ user, allRepos }: { user: User; allRepos: any }) {
|
||||
return (
|
||||
<>
|
||||
<div>Protected content for {user.email}</div>
|
||||
<p>Data fetched with provider token:</p>
|
||||
<pre>{JSON.stringify(allRepos, null, 2)}</pre>
|
||||
<p>user:</p>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
// Create authenticated Supabase Client
|
||||
const supabase = createPagesServerClient(ctx)
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
if (!session)
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/',
|
||||
permanent: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Retrieve provider_token & logged in user's third-party id from metadata
|
||||
const { provider_token, user } = session
|
||||
const userId = user.user_metadata.user_name
|
||||
|
||||
const allRepos = await (
|
||||
await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `token ${provider_token}`,
|
||||
},
|
||||
})
|
||||
).json()
|
||||
|
||||
return { props: { user, allRepos } }
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Protecting API routes
|
||||
|
||||
Create a server supabase client to retrieve the logged in user's session:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx pages/api/protected-route.js
|
||||
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
const ProtectedRoute = async (req, res) => {
|
||||
// Create authenticated Supabase Client
|
||||
const supabase = createPagesServerClient({ req, res })
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
if (!session)
|
||||
return res.status(401).json({
|
||||
error: 'not_authenticated',
|
||||
description: 'The user does not have an active session or is not authenticated',
|
||||
})
|
||||
|
||||
// Run queries with RLS on the server
|
||||
const { data } = await supabase.from('test').select('*')
|
||||
res.json(data)
|
||||
}
|
||||
|
||||
export default ProtectedRoute
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx pages/api/protected-route.ts
|
||||
import { NextApiHandler } from 'next'
|
||||
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
const ProtectedRoute: NextApiHandler = async (req, res) => {
|
||||
// Create authenticated Supabase Client
|
||||
const supabase = createPagesServerClient({ req, res })
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
if (!session)
|
||||
return res.status(401).json({
|
||||
error: 'not_authenticated',
|
||||
description: 'The user does not have an active session or is not authenticated',
|
||||
})
|
||||
|
||||
// Run queries with RLS on the server
|
||||
const { data } = await supabase.from('test').select('*')
|
||||
res.json(data)
|
||||
}
|
||||
|
||||
export default ProtectedRoute
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Auth with Next.js Middleware
|
||||
|
||||
As an alternative to protecting individual pages you can use a [Next.js Middleware](https://nextjs.org/docs/middleware) to protect the entire directory or those that match the config object. In the following example, all requests to `/middleware-protected/*` will check whether a user is signed in, if successful the request will be forwarded to the destination route, otherwise the user will be redirected:
|
||||
|
||||
```ts middleware.ts
|
||||
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
// We need to create a response and hand it to the supabase client to be able to modify the response headers.
|
||||
const res = NextResponse.next()
|
||||
// Create authenticated Supabase Client.
|
||||
const supabase = createMiddlewareClient({ req, res })
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
// Check auth condition
|
||||
if (session?.user.email?.endsWith('@gmail.com')) {
|
||||
// Authentication successful, forward request to protected route.
|
||||
return res
|
||||
}
|
||||
|
||||
// Auth condition not met, redirect to home page.
|
||||
const redirectUrl = req.nextUrl.clone()
|
||||
redirectUrl.pathname = '/'
|
||||
redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
|
||||
return NextResponse.redirect(redirectUrl)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: '/middleware-protected/:path*',
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Migrating to v0.7.X
|
||||
|
||||
#### PKCE Auth Flow
|
||||
|
||||
PKCE is the new server-side auth flow implemented by the Next.js Auth Helpers. It requires a new API route for `/api/auth/callback` that exchanges an auth `code` for the user's `session`.
|
||||
|
||||
Check the [Code Exchange API Route steps](/docs/guides/auth/auth-helpers/nextjs-pages#code-exchange-api-route) above to implement this route.
|
||||
|
||||
#### Authentication
|
||||
|
||||
For authentication methods that have a `redirectTo` or `emailRedirectTo`, this must be set to this new code exchange API Route - `/api/auth/callback`. This is an example with the `signUp` function:
|
||||
|
||||
```jsx
|
||||
supabase.auth.signUp({
|
||||
email: 'jon@example.com',
|
||||
password: 'sup3rs3cur3',
|
||||
options: {
|
||||
emailRedirectTo: 'http://localhost:3000/auth/callback',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
#### Deprecated Functions
|
||||
|
||||
With v0.7.x of the Next.js Auth Helpers a new naming convention has been implemented for createClient functions. The `createBrowserSupabaseClient` and `createServerSupabaseClient` functions have been marked as deprecated, and will be removed in a future version of the Auth Helpers.
|
||||
|
||||
- `createBrowserSupabaseClient` has been replaced with `createPagesBrowserClient`
|
||||
- `createServerSupabaseClient` has been replaced with `createPagesServerClient`
|
||||
|
||||
### Migrating to v0.5.X
|
||||
|
||||
To make these helpers more flexible as well as more maintainable and easier to upgrade for new versions of Next.js, we're stripping them down to the most useful part which is managing the cookies and giving you an authenticated supabase-js client in any environment (client, server, middleware/edge).
|
||||
|
||||
Therefore we're marking the `withApiAuth`, `withPageAuth`, and `withMiddlewareAuth` higher order functions as deprecated and they will be removed in the next **minor** release (v0.6.X).
|
||||
|
||||
Please follow the steps below to update your API routes, pages, and middleware handlers. Thanks!
|
||||
|
||||
#### `withApiAuth` deprecated!
|
||||
|
||||
Use `createPagesServerClient` within your `NextApiHandler`:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="before"
|
||||
>
|
||||
<TabPanel id="before" label="Before">
|
||||
|
||||
```tsx pages/api/protected-route.ts
|
||||
import { withApiAuth } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
export default withApiAuth(async function ProtectedRoute(req, res, supabase) {
|
||||
// Run queries with RLS on the server
|
||||
const { data } = await supabase.from('test').select('*')
|
||||
res.json(data)
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="after" label="After">
|
||||
|
||||
```tsx pages/api/protected-route.ts
|
||||
import { NextApiHandler } from 'next'
|
||||
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
const ProtectedRoute: NextApiHandler = async (req, res) => {
|
||||
// Create authenticated Supabase Client
|
||||
const supabase = createPagesServerClient({ req, res })
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
if (!session)
|
||||
return res.status(401).json({
|
||||
error: 'not_authenticated',
|
||||
description: 'The user does not have an active session or is not authenticated',
|
||||
})
|
||||
|
||||
// Run queries with RLS on the server
|
||||
const { data } = await supabase.from('test').select('*')
|
||||
res.json(data)
|
||||
}
|
||||
|
||||
export default ProtectedRoute
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
#### `withPageAuth` deprecated!
|
||||
|
||||
Use `createPagesServerClient` within `getServerSideProps`:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="before"
|
||||
>
|
||||
<TabPanel id="before" label="Before">
|
||||
|
||||
```tsx pages/profile.tsx
|
||||
import { withPageAuth, User } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
export default function Profile({ user }: { user: User }) {
|
||||
return <pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
}
|
||||
|
||||
export const getServerSideProps = withPageAuth({ redirectTo: '/' })
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="after" label="After">
|
||||
|
||||
```tsx pages/profile.js
|
||||
import { createPagesServerClient, User } from '@supabase/auth-helpers-nextjs'
|
||||
import { GetServerSidePropsContext } from 'next'
|
||||
|
||||
export default function Profile({ user }: { user: User }) {
|
||||
return <pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
// Create authenticated Supabase Client
|
||||
const supabase = createPagesServerClient(ctx)
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
if (!session)
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/',
|
||||
permanent: false,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialSession: session,
|
||||
user: session.user,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
#### `withMiddlewareAuth` deprecated!
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="before"
|
||||
>
|
||||
<TabPanel id="before" label="Before">
|
||||
|
||||
```tsx middleware.ts
|
||||
import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
export const middleware = withMiddlewareAuth({
|
||||
redirectTo: '/',
|
||||
authGuard: {
|
||||
isPermitted: async (user) => {
|
||||
return user.email?.endsWith('@gmail.com') ?? false
|
||||
},
|
||||
redirectTo: '/insufficient-permissions',
|
||||
},
|
||||
})
|
||||
|
||||
export const config = {
|
||||
matcher: '/middleware-protected',
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="after" label="After">
|
||||
|
||||
```tsx middleware.ts
|
||||
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
// We need to create a response and hand it to the supabase client to be able to modify the response headers.
|
||||
const res = NextResponse.next()
|
||||
// Create authenticated Supabase Client.
|
||||
const supabase = createMiddlewareClient({ req, res })
|
||||
// Check if we have a session
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
// Check auth condition
|
||||
if (session?.user.email?.endsWith('@gmail.com')) {
|
||||
// Authentication successful, forward request to protected route.
|
||||
return res
|
||||
}
|
||||
|
||||
// Auth condition not met, redirect to home page.
|
||||
const redirectUrl = req.nextUrl.clone()
|
||||
redirectUrl.pathname = '/'
|
||||
redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
|
||||
return NextResponse.redirect(redirectUrl)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: '/middleware-protected',
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Migrating to v0.4.X and supabase-js v2
|
||||
|
||||
With the update to `supabase-js` v2 the `auth` API routes are no longer required, therefore you can go ahead and delete your `auth` directory under the `/pages/api/` directory. Please refer to the [v2 migration guide](/docs/reference/javascript/v1/upgrade-guide) for the full set of changes within supabase-js.
|
||||
|
||||
The `/api/auth/logout` API route has been removed, please use the `signout` method instead:
|
||||
|
||||
```jsx
|
||||
<button
|
||||
onClick={async () => {
|
||||
await supabaseClient.auth.signOut()
|
||||
router.push('/')
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
```
|
||||
|
||||
The `supabaseClient` and `supabaseServerClient` have been removed in favor of the `createPagesBrowserClient` and `createPagesServerClient` methods. This allows you to provide the CLI-generated types to the client:
|
||||
|
||||
```tsx
|
||||
// client-side
|
||||
import type { Database } from 'types_db'
|
||||
const [supabaseClient] = useState(() => createPagesBrowserClient<Database>())
|
||||
|
||||
// server-side API route
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type { Database } from 'types_db'
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const supabaseServerClient = createPagesServerClient<Database>({
|
||||
req,
|
||||
res,
|
||||
})
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabaseServerClient.auth.getUser()
|
||||
|
||||
res.status(200).json({ name: user?.name ?? '' })
|
||||
}
|
||||
```
|
||||
|
||||
- The `UserProvider` has been replaced by the `SessionContextProvider`. Make sure to wrap your `pages/_app.js` componenent with the `SessionContextProvider`. Then, throughout your application you can use the `useSessionContext` hook to get the `session` and the `useSupabaseClient` hook to get an authenticated `supabaseClient`.
|
||||
- The `useUser` hook now returns the `user` object or `null`.
|
||||
- Usage with TypeScript: You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion:
|
||||
|
||||
Creating a new supabase client object:
|
||||
|
||||
```tsx
|
||||
import { Database } from '../database.types'
|
||||
|
||||
const [supabaseClient] = useState(() => createPagesBrowserClient<Database>())
|
||||
```
|
||||
|
||||
Retrieving a supabase client object from the SessionContext:
|
||||
|
||||
```tsx
|
||||
import { useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import { Database } from '../database.types'
|
||||
|
||||
const supabaseClient = useSupabaseClient<Database>()
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -1,500 +0,0 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'nextjs-server-components',
|
||||
title: 'Supabase Auth with Next.js app directory',
|
||||
description:
|
||||
'Authentication helpers for creating an authenticated Supabase client in Next.js 13 app directory Server Components and Route Handlers.',
|
||||
sidebar_label: 'Next.js (app)',
|
||||
}
|
||||
|
||||
The Next.js Auth Helpers package configures Supabase Auth to store the user's session in a cookie, rather than `localStorage`. This makes the users's session available server-side - in Server Components and Route Handlers - and is automatically sent along with any requests to Supabase.
|
||||
|
||||
> Note: If you are using the `pages` directory, check out [Auth Helpers in Next.js](/docs/guides/auth/auth-helpers/nextjs).
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/Bh1TOpOcGJQ"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
> To learn more about Supabase and the Next.js 13 app directory, check out [this playlist](https://youtube.com/playlist?list=PL5S4mPUpp4OtwG-qCxm8gA_hjaBq0OPdz).
|
||||
|
||||
## Install the Next.js helper library
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="npm"
|
||||
>
|
||||
|
||||
<TabPanel id="npm" label="npm">
|
||||
|
||||
```sh
|
||||
npm install @supabase/auth-helpers-nextjs
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="yarn" label="Yarn">
|
||||
|
||||
```sh
|
||||
yarn add @supabase/auth-helpers-nextjs
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Set up environment variables
|
||||
|
||||
Retrieve your project's URL and anon key from your [API settings](https://app.supabase.com/project/_/settings/api) in the dashboard, and create a `.env.local` file with the following environment variables:
|
||||
|
||||
```bash title=".env.local"
|
||||
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
```
|
||||
|
||||
## Configure Middleware
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Middleware runs immediately before each route in rendered. Next.js only provides read access to headers and cookies in Server Components and Route Handlers, however, Supabase needs to be able to set cookies and headers to refresh expired access tokens. Therefore, you must call the `getSession` function in `middleware.js` in order to use a Supabase client in Server Components or Route Handlers.
|
||||
|
||||
Create a new `middleware.js` file in the root of your project and populate with the following:
|
||||
|
||||
```jsx title="middleware.js"
|
||||
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function middleware(req) {
|
||||
const res = NextResponse.next()
|
||||
const supabase = createMiddlewareSupabaseClient({ req, res })
|
||||
await supabase.auth.getSession()
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Middleware runs immediately before each route in rendered. Next.js only provides read access to headers and cookies in Server Components and Route Handlers, however, Supabase needs to be able to set cookies and headers to refresh expired access tokens. Therefore, you must call the `getSession` function in `middleware.ts` in order to use a Supabase client in Server Components or Route Handlers.
|
||||
|
||||
Create a new `middleware.ts` file in the root of your project and populate with the following:
|
||||
|
||||
```tsx title="middleware.ts"
|
||||
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
import type { NextRequest } from 'next/server'
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
const res = NextResponse.next()
|
||||
const supabase = createMiddlewareSupabaseClient<Database>({ req, res })
|
||||
await supabase.auth.getSession()
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createMiddlewareSupabaseClient` to add type support to the Supabase client.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Supabase Provider
|
||||
|
||||
All Client Components need to share a single instance of the Supabase client. We can wrap our application in a `<SupabaseProvider />` and use React Context to create a global Supabase instance.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Create a new file at `/app/supabase-provider.jsx` and populate with the following:
|
||||
|
||||
```jsx title="app/supabase-provider.jsx"
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
const Context = createContext(undefined)
|
||||
|
||||
export default function SupabaseProvider({ children, session }) {
|
||||
const [supabase] = useState(() => createBrowserSupabaseClient())
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
router.refresh()
|
||||
})
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}, [router, supabase])
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ supabase, session }}>
|
||||
<>{children}</>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useSupabase = () => {
|
||||
const context = useContext(Context)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useSupabase must be used inside SupabaseProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create a new file at `/app/supabase-provider.tsx` and populate with the following:
|
||||
|
||||
```tsx title="app/supabase-provider.tsx"
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { Session, createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import type { SupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
type MaybeSession = Session | null
|
||||
|
||||
type SupabaseContext = {
|
||||
supabase: SupabaseClient<Database>
|
||||
session: MaybeSession
|
||||
}
|
||||
|
||||
const Context = createContext<SupabaseContext | undefined>(undefined)
|
||||
|
||||
export default function SupabaseProvider({
|
||||
children,
|
||||
session,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
session: MaybeSession
|
||||
}) {
|
||||
const [supabase] = useState(() => createBrowserSupabaseClient())
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
router.refresh()
|
||||
})
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}, [router, supabase])
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ supabase, session }}>
|
||||
<>{children}</>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useSupabase = () => {
|
||||
const context = useContext(Context)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useSupabase must be used inside SupabaseProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
```
|
||||
|
||||
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createBrowserSupabaseClient` to add type support to the Supabase client.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Modify `layout.jsx` to wrap the application with the `<SupabaseProvider>` component:
|
||||
|
||||
```jsx title="app/layout.jsx"
|
||||
import './globals.css'
|
||||
import SupabaseProvider from './supabase-provider'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<SupabaseProvider session={session}>
|
||||
<SupabaseListener serverAccessToken={session?.access_token} />
|
||||
{children}
|
||||
</SupabaseProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Modify `layout.tsx` to wrap the application with the `<SupabaseProvider>` component:
|
||||
|
||||
```tsx title="app/layout.tsx"
|
||||
import './globals.css'
|
||||
import SupabaseProvider from './supabase-provider'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<SupabaseProvider session={session}>
|
||||
<SupabaseListener serverAccessToken={session?.access_token} />
|
||||
{children}
|
||||
</SupabaseProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
Now any of our Client Components can use the `useSupabase` hook to ensure they are using the same instance of a Supabase client.
|
||||
|
||||
## Creating a Supabase Client
|
||||
|
||||
### Client Components
|
||||
|
||||
While Server Components are great for data fetching, we still need to use Supabase client-side for [authentication](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login.tsx) and [realtime subscriptions](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx).
|
||||
|
||||
As mentioned above, it is important that all Client Components share a single instance of the Supabase client. We can use the `useSupabase` hook we created above to ensure this is the case.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title="app/new-post.jsx"
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useSupabase } from './supabase-provider'
|
||||
|
||||
export default function NewPost() {
|
||||
const [content, setContent] = useState('')
|
||||
const { supabase } = useSupabase()
|
||||
|
||||
const handleSave = async () => {
|
||||
const { data } = await supabase.from('posts').insert({ content }).select()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<input onChange={(e) => setContent(e.target.value)} value={content} />
|
||||
<button onClick={handleSave}>Save</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```jsx title="app/new-post.tsx"
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useSupabase } from './supabase-provider'
|
||||
|
||||
export default function NewPost() {
|
||||
const [content, setContent] = useState('')
|
||||
const { supabase } = useSupabase()
|
||||
|
||||
const handleSave = async () => {
|
||||
const { data } = await supabase.from('posts').insert({ content }).select()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<input onChange={(e) => setContent(e.target.value)} value={content} />
|
||||
<button onClick={handleSave}>Save</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
> check out [this example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/supabase-provider.tsx) for making the user's session available to all Client Components.
|
||||
|
||||
### Server Components
|
||||
|
||||
In order to use Supabase in Server Components, you need to have implemented the `middleware.ts` steps above 👆
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title="app/page.jsx"
|
||||
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { headers, cookies } from 'next/headers'
|
||||
|
||||
// do not cache this page
|
||||
export const revalidate = 0
|
||||
|
||||
export default async function ServerComponent() {
|
||||
const supabase = createServerComponentSupabaseClient({
|
||||
headers,
|
||||
cookies,
|
||||
})
|
||||
const { data } = await supabase.from('posts').select('*')
|
||||
|
||||
return <pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title="app/page.tsx"
|
||||
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { headers, cookies } from 'next/headers'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
// do not cache this page
|
||||
export const revalidate = 0
|
||||
|
||||
export default async function ServerComponent() {
|
||||
const supabase = createServerComponentSupabaseClient<Database>({
|
||||
headers,
|
||||
cookies,
|
||||
})
|
||||
const { data } = await supabase.from('posts').select('*')
|
||||
|
||||
return <pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
> check out [this example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/page.tsx) for redirecting unauthenticated users - protected pages.
|
||||
|
||||
### Route Handlers
|
||||
|
||||
In order to use Supabase in Route Handlers, you need to have implemented the `middleware.ts` steps above 👆
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title="app/api/posts/route.jsx"
|
||||
import { createRouteHandlerSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { headers, cookies } from 'next/headers'
|
||||
|
||||
// do not cache this page
|
||||
export const revalidate = 0
|
||||
|
||||
export async function GET() {
|
||||
const supabase = createRouteHandlerSupabaseClient({
|
||||
headers,
|
||||
cookies,
|
||||
})
|
||||
const { data } = await supabase.from('posts').select('*')
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title="app/api/posts/route.tsx"
|
||||
import { createRouteHandlerSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { headers, cookies } from 'next/headers'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
// do not cache this page
|
||||
export const revalidate = 0
|
||||
|
||||
export async function GET() {
|
||||
const supabase = createRouteHandlerSupabaseClient<Database>({
|
||||
headers,
|
||||
cookies,
|
||||
})
|
||||
const { data } = await supabase.from('posts').select('*')
|
||||
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
> Check out [this repo](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) for a full example including [authentication](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login.tsx), [realtime](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx) and [protected pages](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/page.tsx).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
File diff suppressed because it is too large.
Load diff
@@ -56,11 +56,87 @@ This library supports the following tooling versions:
|
||||
|
||||
Retrieve your project URL and anon key in your project's [API settings](https://app.supabase.com/project/_/settings/api) in the Dashboard to set up the following environment variables. For local development you can set them in a `.env` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/remix/.env.example).
|
||||
|
||||
```bash title=.env
|
||||
```bash .env
|
||||
SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
```
|
||||
|
||||
### Code Exchange Route
|
||||
|
||||
The `Code Exchange` route is required for the [server-side auth flow](https://supabase.com/docs/guides/auth/server-side-rendering) implemented by the Remix Auth Helpers. It exchanges an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Create a new file at `app/routes/auth.callback.jsx` and populate with the following:
|
||||
|
||||
```jsx app/routes/auth.callback.jsx
|
||||
import { redirect } from '@remix-run/node'
|
||||
import { createServerClient } from '@supabase/auth-helpers-remix'
|
||||
|
||||
export const loader = async ({ request }) => {
|
||||
const response = new Response()
|
||||
const url = new URL(request.url)
|
||||
const code = url.searchParams.get('code')
|
||||
|
||||
if (code) {
|
||||
const supabaseClient = createServerClient(
|
||||
process.env.SUPABASE_URL,
|
||||
process.env.SUPABASE_ANON_KEY,
|
||||
{ request, response }
|
||||
)
|
||||
await supabaseClient.auth.exchangeCodeForSession(code)
|
||||
}
|
||||
|
||||
return redirect('/', {
|
||||
headers: response.headers,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create a new file at `app/routes/auth.callback.tsx` and populate with the following:
|
||||
|
||||
```tsx app/routes/auth.callback.tsx
|
||||
import { redirect } from '@remix-run/node'
|
||||
import { createServerClient } from '@supabase/auth-helpers-remix'
|
||||
|
||||
import type { Database } from 'db_types'
|
||||
import type { LoaderArgs } from '@remix-run/node'
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
const response = new Response()
|
||||
const url = new URL(request.url)
|
||||
const code = url.searchParams.get('code')
|
||||
|
||||
if (code) {
|
||||
const supabaseClient = createServerClient<Database>(
|
||||
process.env.SUPABASE_URL!,
|
||||
process.env.SUPABASE_ANON_KEY!,
|
||||
{ request, response }
|
||||
)
|
||||
await supabaseClient.auth.exchangeCodeForSession(code)
|
||||
}
|
||||
|
||||
return redirect('/', {
|
||||
headers: response.headers,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
> `Database` is a TypeScript definitions file [generated by the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types).
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Server-side
|
||||
|
||||
The Supabase client can now be used server-side - in loaders and actions - by calling the `createServerClient` function.
|
||||
@@ -252,7 +328,7 @@ Since our environment variables are not available client-side, we need to plumb
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title=app/root.jsx
|
||||
```jsx app/root.jsx
|
||||
export const loader = () => {
|
||||
const env = {
|
||||
SUPABASE_URL: process.env.SUPABASE_URL,
|
||||
@@ -267,26 +343,26 @@ export const loader = () => {
|
||||
|
||||
Next, we call the `useLoaderData` hook in our component to get the `env` object.
|
||||
|
||||
```jsx title=app/root.jsx
|
||||
```jsx app/root.jsx
|
||||
const { env } = useLoaderData()
|
||||
```
|
||||
|
||||
We then want to instantiate a single instance of a Supabase browser client, to be used across our client-side components.
|
||||
|
||||
```jsx title=app/root.jsx
|
||||
```jsx app/root.jsx
|
||||
const [supabase] = useState(() => createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY))
|
||||
```
|
||||
|
||||
And then we can share this instance across our application with Outlet Context.
|
||||
|
||||
```jsx title=app/root.jsx
|
||||
```jsx app/root.jsx
|
||||
<Outlet context={{ supabase }} />
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title=app/root.tsx
|
||||
```tsx app/root.tsx
|
||||
export const loader = ({}: LoaderArgs) => {
|
||||
const env = {
|
||||
SUPABASE_URL: process.env.SUPABASE_URL!,
|
||||
@@ -301,13 +377,13 @@ export const loader = ({}: LoaderArgs) => {
|
||||
|
||||
Next, we call the `useLoaderData` hook in our component to get the `env` object.
|
||||
|
||||
```tsx title=app/root.tsx
|
||||
```tsx app/root.tsx
|
||||
const { env } = useLoaderData<typeof loader>()
|
||||
```
|
||||
|
||||
We then want to instantiate a single instance of a Supabase browser client, to be used across our client-side components.
|
||||
|
||||
```tsx title=app/root.tsx
|
||||
```tsx app/root.tsx
|
||||
const [supabase] = useState(() =>
|
||||
createBrowserClient<Database>(env.SUPABASE_URL, env.SUPABASE_ANON_KEY)
|
||||
)
|
||||
@@ -315,7 +391,7 @@ const [supabase] = useState(() =>
|
||||
|
||||
And then we can share this instance across our application with Outlet Context.
|
||||
|
||||
```tsx title=app/root.tsx
|
||||
```tsx app/root.tsx
|
||||
<Outlet context={{ supabase }} />
|
||||
```
|
||||
|
||||
@@ -341,7 +417,7 @@ Let's pipe that through from our loader.
|
||||
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title=app/root.jsx
|
||||
```jsx app/root.jsx
|
||||
export const loader = async ({ request }) => {
|
||||
const env = {
|
||||
SUPABASE_URL: process.env.SUPABASE_URL,
|
||||
@@ -375,7 +451,7 @@ export const loader = async ({ request }) => {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title=app/root.tsx
|
||||
```tsx app/root.tsx
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
const env = {
|
||||
SUPABASE_URL: process.env.SUPABASE_URL!,
|
||||
@@ -420,7 +496,7 @@ And then use the revalidator, inside the `onAuthStateChange` hook.
|
||||
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title=app/root.jsx
|
||||
```jsx app/root.jsx
|
||||
const { env, session } = useLoaderData()
|
||||
const { revalidate } = useRevalidator()
|
||||
|
||||
@@ -441,14 +517,14 @@ useEffect(() => {
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}, [serverAccessToken, supabase, fetcher])
|
||||
}, [serverAccessToken, supabase, revalidate])
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title=app/root.tsx
|
||||
```tsx app/root.tsx
|
||||
const { env, session } = useLoaderData<typeof loader>()
|
||||
const { revalidate } = useRevalidator()
|
||||
|
||||
@@ -471,7 +547,7 @@ useEffect(() => {
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}, [serverAccessToken, supabase, fetcher])
|
||||
}, [serverAccessToken, supabase, revalidate])
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -493,7 +569,7 @@ Now we can use our outlet context to access our single instance of Supabase and
|
||||
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title=app/components/login.jsx
|
||||
```jsx app/components/login.jsx
|
||||
export default function Login() {
|
||||
const { supabase } = useOutletContext()
|
||||
|
||||
@@ -507,6 +583,9 @@ export default function Login() {
|
||||
const handleGitHubLogin = async () => {
|
||||
await supabase.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
options: {
|
||||
redirectTo: 'http://localhost:3000/auth/callback',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -528,7 +607,7 @@ export default function Login() {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title=app/components/login.tsx
|
||||
```tsx app/components/login.tsx
|
||||
export default function Login() {
|
||||
const { supabase } = useOutletContext<{ supabase: SupabaseClient<Database> }>()
|
||||
|
||||
@@ -542,6 +621,9 @@ export default function Login() {
|
||||
const handleGitHubLogin = async () => {
|
||||
await supabase.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
options: {
|
||||
redirectTo: 'http://localhost:3000/auth/callback',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -574,7 +656,7 @@ export default function Login() {
|
||||
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title=app/routes/realtime.jsx
|
||||
```jsx app/routes/realtime.jsx
|
||||
import { useLoaderData, useOutletContext } from '@remix-run/react'
|
||||
import { createServerClient } from '@supabase/auth-helpers-remix'
|
||||
import { json } from '@remix-run/node'
|
||||
@@ -622,7 +704,7 @@ export default function Index() {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title=app/routes/realtime.tsx
|
||||
```tsx app/routes/realtime.tsx
|
||||
import { useLoaderData, useOutletContext } from '@remix-run/react'
|
||||
import { createServerClient } from '@supabase/auth-helpers-remix'
|
||||
import { json } from '@remix-run/node'
|
||||
@@ -685,6 +767,30 @@ export default function Index() {
|
||||
|
||||
> Ensure you have [enabled replication](https://app.supabase.com/project/_/database/replication) on the table you are subscribing to.
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Migrating to v0.2.0
|
||||
|
||||
#### PKCE Auth Flow
|
||||
|
||||
PKCE is the new server-side auth flow implemented by the Remix Auth Helpers. It requires a new `loader` route for `/auth/callback` that exchanges an auth `code` for the user's `session`.
|
||||
|
||||
Check the [Code Exchange Route steps](/docs/guides/auth/auth-helpers/remix#code-exchange-route) above to implement this route.
|
||||
|
||||
#### Authentication
|
||||
|
||||
For authentication methods that have a `redirectTo` or `emailRedirectTo`, this must be set to this new code exchange API Route - `/api/auth/callback`. This is an example with the `signUp` function:
|
||||
|
||||
```jsx
|
||||
supabaseClient.auth.signUp({
|
||||
email: 'jon@example.com',
|
||||
password: 'sup3rs3cur3',
|
||||
options: {
|
||||
emailRedirectTo: 'http://localhost:3000/auth/callback',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
File diff suppressed because it is too large.
Load diff
@@ -88,7 +88,7 @@ Future<void> signOut() async {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ We are using `next` as our query parameter, but this can name whatever you like.
|
||||
The email link you receive will behave like a magic link. When the link is clicked you will be sent to the `redirectTo` URL you specified that points to the path with the exchange code.
|
||||
|
||||
### Exchange authorization code
|
||||
After redirecting to the server page, we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeAuthCodeForSession` function.
|
||||
After redirecting to the server page, we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeCodeForSession` function.
|
||||
```ts
|
||||
// api/auth/callback.ts
|
||||
|
||||
|
||||
@@ -104,6 +104,8 @@ create policy "Users can update their own profiles."
|
||||
2. Enables RLS.
|
||||
3. Creates a policy which allows logged in users to update their own data.
|
||||
|
||||
**Note:** If you want to use upsert operations, the user needs to have `INSERT`, `UPDATE`, and `SELECT` permissions.
|
||||
|
||||
### Only anon or authenticated access
|
||||
|
||||
You can add a Postgres role
|
||||
@@ -286,43 +288,24 @@ using (
|
||||
|
||||
## Tips
|
||||
|
||||
### Enable Realtime for database tables
|
||||
|
||||
Realtime server broadcasts database changes to authorized users depending on your Row Level Security (RLS) policies.
|
||||
We recommend that you enable row level security and set row security policies on tables that you add to the publication.
|
||||
However, you may choose to disable RLS on a table and have changes broadcast to all connected clients.
|
||||
|
||||
```sql
|
||||
/**
|
||||
* REALTIME SUBSCRIPTIONS
|
||||
* Realtime enables listening to any table in your public schema.
|
||||
*/
|
||||
|
||||
begin;
|
||||
-- remove the realtime publication
|
||||
drop publication if exists supabase_realtime;
|
||||
|
||||
-- re-create the publication but don't enable it for any tables
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
|
||||
-- add a table to the publication
|
||||
alter publication supabase_realtime add table products;
|
||||
|
||||
-- add other tables to the publication
|
||||
alter publication supabase_realtime add table posts;
|
||||
```
|
||||
|
||||
### You don't have to use policies
|
||||
|
||||
You can also put your authorization rules in your middleware, similar to how you would create security rules with any other `backend <-> middleware <-> frontend` architecture.
|
||||
You can also put your authorization rules in your middleware, similar to how you would create security rules with any other `backend <-> middleware <-> frontend` architecture. You can use Edge Functions to run this architecture, or you can use your favorite server framework, like Rails, Django, Node.js, Phoenix, or Laravel.
|
||||
|
||||
Policies are a tool. In the case of "serverless/Jamstack" setups, they are especially effective because you don't have to deploy any middleware at all.
|
||||
Policies are a tool. In the case of "serverless/Jamstack" setups, they are especially effective because you don't have to deploy any middleware at all and can use the javascript libraries directly from the browser.
|
||||
|
||||
However, if you want to use another authorization method for your applications, that's also fine. Supabase is "just Postgres", so if your application
|
||||
works with Postgres, then it also works with Supabase.
|
||||
That said, if you want to use another authorization method for your applications that's also fine. Supabase is "just Postgres", so if your application works with Postgres, then it also works with Supabase.
|
||||
|
||||
Tip: Make sure to enable RLS for all your tables, so that your tables are inaccessible. Then use the "Service" which we provide, which is designed to bypass RLS.
|
||||
If you plan to use this approach make sure to enable RLS for your tables. Then use the `service_role` key (for our client libraries) or the `postgres` role - both of these can bypass RLS. You don't need to create any policies with this approach, simply enabling RLS is sufficient:
|
||||
|
||||
```sql
|
||||
create table profiles (
|
||||
id serial primary key,
|
||||
email text
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
```
|
||||
|
||||
### Never use a service key on the client
|
||||
|
||||
|
||||
@@ -306,3 +306,7 @@ export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
|
||||
### Which authentication flows have PKCE support?
|
||||
|
||||
At present, PKCE is supported on the Magic Link, OAuth, Sign Up, and Password Recovery routes. These correspond to the `signInWithOtp`, `signInWithOAuth`, `signUp`, and `resetPasswordForEmail` methods on the Supabase client library. When using PKCE with Phone and Email OTPs, there is no behavior change with respect to the implicit flow - an access token will be returned in the body when a request is successful.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import AppleSecretGenerator from '~/components/AppleSecretGenerator'
|
||||
|
||||
export const meta = {
|
||||
id: 'auth-apple',
|
||||
@@ -89,69 +90,15 @@ Now you'll need to download a `secret key` file from Apple that will be used to
|
||||
- Save the downloaded file -- this contains your "secret key" that will be used to generate your `client_secret`.
|
||||
- Click `Done` at the top right.
|
||||
|
||||
## Generate a `client_secret`
|
||||
## Generate a client secret
|
||||
|
||||
The `secret key` you downloaded is used to create the `client_secret` string you'll need to authenticate your users.
|
||||
You need to configure a client secret when using Sign in with Apple for Web. This is a specially crafted [JWT signed with a secret key downloaded from Apple's Developer Center](https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens).
|
||||
|
||||
According to the [Apple Docs](https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens) it needs to be a JWT
|
||||
token encrypted using the Elliptic Curve Digital Signature Algorithm (ECDSA) with the P-256 curve and the SHA-256 hash algorithm.
|
||||
<Admonition>
|
||||
Use this tool to generate a new Apple client secret. No keys leave your browser!
|
||||
</Admonition>
|
||||
|
||||
At this time, the easiest way to generate this JWT token is with [Ruby](https://www.ruby-lang.org/en/).
|
||||
If you don't have Ruby installed, you can [Download Ruby Here](https://www.ruby-lang.org/en/downloads).
|
||||
|
||||
- Install Ruby (or check to make sure it's installed on your system).
|
||||
- Install [ruby-jwt](https://github.com/jwt/ruby-jwt).
|
||||
- From the command line, run: `sudo gem install jwt`.
|
||||
|
||||
Create the script below using a text editor: `secret_gen.rb`
|
||||
|
||||
```ruby
|
||||
require "jwt"
|
||||
|
||||
key_file = "Path to the private key"
|
||||
team_id = "Your Team ID"
|
||||
client_id = "The Service ID of the service you created"
|
||||
key_id = "The Key ID of the private key"
|
||||
|
||||
validity_period = 180 # In days. Max 180 (6 months) according to Apple docs.
|
||||
|
||||
private_key = OpenSSL::PKey::EC.new IO.read key_file
|
||||
|
||||
token = JWT.encode(
|
||||
{
|
||||
iss: team_id,
|
||||
iat: Time.now.to_i,
|
||||
exp: Time.now.to_i + 86400 * validity_period,
|
||||
aud: "https://appleid.apple.com",
|
||||
sub: client_id
|
||||
},
|
||||
private_key,
|
||||
"ES256",
|
||||
header_fields=
|
||||
{
|
||||
kid: key_id
|
||||
}
|
||||
)
|
||||
puts token
|
||||
```
|
||||
|
||||
1. Edit the `secret_gen.rb` file:
|
||||
|
||||
- `key_file` = "Path to the private key you downloaded from Apple". It should look like this: `AuthKey_XXXXXXXXXX.p8`.
|
||||
- `team_id` = "Your Team ID". This is found at the Apple Developer website, under Membership details. This is a 10-character alphanumeric string called "Team ID". Alternatively, this can be seen next to your name in the upper right when viewing your Certificates, Identifiers & Profiles.
|
||||
- `client_id` = "The Service ID of the service you created". This is the `Services ID` you created in the above step `Obtain a Services ID`. If you've lost this ID, you can find it in the Apple Developer Site:
|
||||
- Go to `Certificates, Identifiers & Profiles`.
|
||||
- Click `Identifiers` at the left.
|
||||
- At the top right drop-down, select `Services IDs`.
|
||||
- Find your Identifier in the list (i.e. app.com.acme.roadrunner).
|
||||
- `key_id` = "The Key ID of the private key". This can be found in the name of your downloaded secret file (For a file named `AuthKey_XXXXXXXXXX.p8` your key_id is `XXXXXXXXXX`). If you've lost this ID, you can find it in the Apple Developer Site:
|
||||
- Go to `Certificates, Identifiers & Profiles`.
|
||||
- Click `Keys` at the left.
|
||||
- Click on your newly-created key in the list.
|
||||
- Look under `Key ID` to find your key_id.
|
||||
|
||||
2. From the command line, run: `ruby secret_gen.rb > client_secret.txt`.
|
||||
3. Your `client_secret` is now stored in this `client_secret.txt` file.
|
||||
<AppleSecretGenerator />
|
||||
|
||||
## Add your OAuth credentials to Supabase
|
||||
|
||||
@@ -180,8 +127,6 @@ async function signout() {
|
||||
## Resources
|
||||
|
||||
- [Apple Developer Account](https://developer.apple.com).
|
||||
- [Ruby](https://www.ruby-lang.org/en/) Docs.
|
||||
- [ruby-jwt](https://github.com/jwt/ruby-jwt) library.
|
||||
- Thanks to [Janak Amarasena](https://medium.com/@janakda) who did all the heavy lifting in [How to configure Sign In with Apple](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -2,11 +2,11 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'auth-azure',
|
||||
title: 'Login with Azure',
|
||||
description: 'Add Azure OAuth to your Supabase project',
|
||||
title: 'Login with Azure (Microsoft)',
|
||||
description: 'Add Azure (Microsoft) OAuth to your Supabase project',
|
||||
}
|
||||
|
||||
To enable Azure Auth for your project, you need to set up an Azure OAuth application and add the application credentials to your Supabase Dashboard.
|
||||
To enable Azure (Microsoft) Auth for your project, you need to set up an Azure OAuth application and add the application credentials to your Supabase Dashboard.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -91,6 +91,21 @@ async function signout() {
|
||||
}
|
||||
```
|
||||
|
||||
## Obtain the provider refresh token
|
||||
|
||||
Azure OAuth2.0 doesn't return the `provider_refresh_token` by default. If you need the `provider_refresh_token` returned, you will need to include the following scope:
|
||||
|
||||
```js
|
||||
async function signInWithAzure() {
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'azure',
|
||||
options: {
|
||||
scopes: 'offline_access',
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Azure Developer Account](https://portal.azure.com)
|
||||
|
||||
@@ -68,7 +68,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Bitbucket Account](https://bitbucket.org)
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Discord Account](https://discord.com)
|
||||
- [Discord Developer Portal](https://discord.com/developers)
|
||||
|
||||
@@ -83,7 +83,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Facebook Developers Dashboard](https://developers.facebook.com/)
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [GitHub Developer Settings](https://github.com/settings/developers)
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [GitLab Account](https://gitlab.com)
|
||||
|
||||
|
||||
@@ -83,22 +83,6 @@ async function signInWithGoogle() {
|
||||
}
|
||||
```
|
||||
|
||||
Google OAuth2.0 doesn't return the `provider_refresh_token` by default. If you need the `provider_refresh_token` returned, you will need to add additional query parameters:
|
||||
|
||||
```js
|
||||
async function signInWithGoogle() {
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
options: {
|
||||
queryParams: {
|
||||
access_type: 'offline',
|
||||
prompt: 'consent',
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
You can view the full list of query parameters and their descriptions [here](https://developers.google.com/identity/protocols/oauth2/web-server#creatingclient).
|
||||
|
||||
When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage:
|
||||
@@ -109,9 +93,28 @@ async function signout() {
|
||||
}
|
||||
```
|
||||
|
||||
## Obtain the provider refresh token
|
||||
|
||||
Google OAuth2.0 doesn't return the `provider_refresh_token` by default. If you need the `provider_refresh_token` returned, you will need to add additional query parameters:
|
||||
|
||||
```js
|
||||
async function signInWithGoogle() {
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
options: {
|
||||
queryParams: {
|
||||
access_type: 'offline',
|
||||
prompt: 'consent',
|
||||
hd: 'domain.com', // google will also allow OAuth logins to be restricted to a specified domain using the 'hd' parameter
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Google Cloud Platform Console](https://console.cloud.google.com/home/dashboard)
|
||||
|
||||
|
||||
Loaded 100 of 969 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user