mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 08:09:11 +03:00
Merge branch 'master' into feat/project-level-permissions
This commit is contained in:
commit
e6603bbecb
1403 files changed
+93825
-57558
No files matched your search
@@ -0,0 +1,19 @@
|
||||
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file.
|
||||
|
||||
YES/NO
|
||||
|
||||
## What kind of change does this PR introduce?
|
||||
|
||||
Bug fix, feature, docs update, ...
|
||||
|
||||
## What is the current behavior?
|
||||
|
||||
Please link any relevant issues here.
|
||||
|
||||
## What is the new behavior?
|
||||
|
||||
Feel free to include screenshots if it includes visual changes.
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context or screenshots.
|
||||
@@ -24,10 +24,6 @@ jobs:
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/ai-commands
|
||||
@@ -37,10 +33,10 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
packages
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
|
||||
@@ -15,10 +15,6 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -26,10 +22,10 @@ jobs:
|
||||
apps/docs
|
||||
packages
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install deps
|
||||
|
||||
@@ -21,16 +21,12 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
- uses: supabase/setup-cli@v1
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -19,9 +19,9 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: apps
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version-file: '.nvmrc'
|
||||
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
|
||||
- name: Download dependencies
|
||||
run: |
|
||||
@@ -42,9 +42,9 @@ jobs:
|
||||
sparse-checkout: |
|
||||
i18n
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version-file: '.nvmrc'
|
||||
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
|
||||
- name: Download dependencies
|
||||
run: |
|
||||
|
||||
@@ -42,9 +42,9 @@ jobs:
|
||||
supabase
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Download dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -25,29 +25,23 @@ jobs:
|
||||
# Uses larger hosted runner as it significantly decreases build times
|
||||
runs-on: [larger-runner-4cpu]
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
cmd:
|
||||
- npm run test:studio
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
apps/studio
|
||||
packages
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
working-directory: ./
|
||||
- name: Run ${{ matrix.cmd }}
|
||||
- name: Run Tests
|
||||
env:
|
||||
# Default is 2 GB, increase to have less frequent OOM errors
|
||||
NODE_OPTIONS: '--max_old_space_size=3072'
|
||||
run: ${{ matrix.cmd }}
|
||||
run: npm run test:studio
|
||||
working-directory: ./
|
||||
@@ -15,18 +15,14 @@ jobs:
|
||||
# Uses larger hosted runner as it significantly decreases build times
|
||||
runs-on: [larger-runner-4cpu]
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install deps
|
||||
|
||||
@@ -15,20 +15,16 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
packages
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install deps
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# CONTRIBUTING.md
|
||||
|
||||
Thank you for contributing to Supabase! We’re a big, exciting open source project and we’d love to have you contribute! Here’s some resources and guidance to help you get started:
|
||||
|
||||
[1. Getting Started](#getting-started)
|
||||
[2. Issues](#issues)
|
||||
[3. Pull Requests](#pull-requests)
|
||||
|
||||
## Getting Started
|
||||
|
||||
To ensure a positive and inclusive environment, please read our [code of conduct](https://github.com/supabase/.github/blob/main/CODE_OF_CONDUCT.md) before contributing. For help setting up the code in this repo, please follow our [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md) file. For the [docs](https://supabase.com/docs) site, follow this [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md) guide.
|
||||
|
||||
## Issues
|
||||
|
||||
If you find a bug, please create an Issue and we’ll triage it.
|
||||
|
||||
- Please search [existing Issues](https://github.com/supabase/supabase/issues) before creating a new one.
|
||||
- Please include a clear description of the problem along with steps to reproduce it. Exact steps with screenshots and urls really help here.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
We actively welcome your Pull Requests! A couple of things to keep in mind before you submit:
|
||||
|
||||
- If you’re fixing an Issue, make sure someone else hasn’t already created a PR fixing the same issue. Likewise, make sure to link your PR to the related Issue(s).
|
||||
- We will always try to accept the first viable PR that resolves the Issue.
|
||||
- If you're new, we encourage you to take a look at issues tagged with [good first issue](https://github.com/supabase/supabase/labels/good%20first%20issue).
|
||||
- If you’re submitting a new feature, make sure you have opened a [Discussion](https://github.com/orgs/supabase/discussions/new/choose) to discuss the new feature before opening a PR. We’d love to accept your hard work, but unfortunately if a feature hasn’t gone through a proper design process, your PR will be closed.
|
||||
- Please use the PR message template and provide detailed context for quicker review. PRs without clear problem statements will be closed.
|
||||
|
||||
Prior to submitting your PR, please conduct the following pre-flight checks:
|
||||
|
||||
- Run `npm run build` locally to ensure that your code builds successfully without having to wait on us to approve Vercel Preview deploys.
|
||||
- Ensure that the Prettier tests run successfully on your PR.
|
||||
|
||||
Running these before you create the PR will help reduce back and forth with the team.
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
# Developing Supabase
|
||||
|
||||
- [Developing Supabase](#developing-supabase)
|
||||
|
||||
- [Getting started](#getting-started)
|
||||
- [Install dependencies](#install-dependencies)
|
||||
- [Local development](#local-development)
|
||||
@@ -11,8 +12,8 @@
|
||||
- [Shared components](#shared-components)
|
||||
- [Installing packages](#installing-packages)
|
||||
- [Running Docker for Supabase Studio](#running-docker-for-supabase-studio)
|
||||
- [Prerequsites](#prerequsites)
|
||||
- [Get Started](#get-started)
|
||||
- [Prerequsites](#prerequsites)
|
||||
- [Get Started](#get-started)
|
||||
- [Create a pull request](#create-a-pull-request)
|
||||
- [Issue assignment](#issue-assignment)
|
||||
- [Common tasks](#common-tasks)
|
||||
@@ -36,8 +37,8 @@ To ensure a positive and inclusive environment, please read our [code of conduct
|
||||
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 v18.x (LTS)](http://nodejs.org)
|
||||
- [npm](https://www.npmjs.com/) version 9.x.x
|
||||
- [Node.js v20.x (LTS)](http://nodejs.org)
|
||||
- [npm](https://www.npmjs.com/) version 9.x.x or higher
|
||||
- [Docker](https://docs.docker.com/get-docker/) (to run studio locally)
|
||||
|
||||
## Local development
|
||||
|
||||
@@ -111,6 +111,15 @@ 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>
|
||||
<tr>
|
||||
<td>Swift</td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/PostgREST" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Auth" target="_blank" rel="noopener noreferrer">auth-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Realtime" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Storage" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Functions" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
|
||||
</tr>
|
||||
<!-- /notranslate -->
|
||||
<th colspan="7">💚 Community 💚</th>
|
||||
<!-- notranslate -->
|
||||
@@ -177,15 +186,6 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swift</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-swift" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-swift" target="_blank" rel="noopener noreferrer">gotrue-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-swift" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-swift" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-swift" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Godot Engine (GDScript)</td>
|
||||
<td><a href="https://github.com/supabase-community/godot-engine.supabase" target="_blank" rel="noopener noreferrer">supabase-gdscript</a></td>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Message as MessageItem } from 'ai/react'
|
||||
import type { Message as MessageItem } from 'ai/react'
|
||||
import Link from 'next/link'
|
||||
import { useParams, usePathname } from 'next/navigation'
|
||||
import { cn } from 'ui'
|
||||
|
||||
@@ -14,16 +14,16 @@
|
||||
"@gregnr/libpg-query": "13.4.0-dev.12",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@supabase/ssr": "^0.1.0",
|
||||
"@tanstack/react-query": "^5.7.2",
|
||||
"@tanstack/react-query": "^5.13.4",
|
||||
"ai": "^2.2.31",
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"eslint-config-next": "^14.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.338.0",
|
||||
"next": "^14.0.3",
|
||||
"openai": "^4.17.0",
|
||||
"next": "^14.2.0",
|
||||
"openai": "^4.20.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-flow": "^1.0.3",
|
||||
@@ -34,12 +34,12 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/node": "^20.11.16",
|
||||
"@types/react": "^18.2.24",
|
||||
"@types/react-dom": "^18.2.8",
|
||||
"autoprefixer": "10.4.14",
|
||||
"nextjs-node-loader": "^1.1.5-alpha.0",
|
||||
"postcss": "8.4.31",
|
||||
"typescript": "^5.2.2"
|
||||
"autoprefixer": "^10.4.14",
|
||||
"nextjs-node-loader": "^1.1.5",
|
||||
"postcss": "^8.4.31",
|
||||
"typescript": "^5.4.3"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"target": "ES2021",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const Favicons = () => {
|
||||
const { basePath } = useRouter()
|
||||
return (
|
||||
<Head>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="57x57" href={`${basePath}/favicon/apple-touch-icon-57x57.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="114x114" href={`${basePath}/favicon/apple-touch-icon-114x114.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="72x72" href={`${basePath}/favicon/apple-touch-icon-72x72.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="144x144" href={`${basePath}/favicon/apple-touch-icon-144x144.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="60x60" href={`${basePath}/favicon/apple-touch-icon-60x60.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="120x120" href={`${basePath}/favicon/apple-touch-icon-120x120.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="76x76" href={`${basePath}/favicon/apple-touch-icon-76x76.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="152x152" href={`${basePath}/favicon/apple-touch-icon-152x152.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-196x196.png`} sizes="196x196"/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-96x96.png`} sizes="96x96"/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-32x32.png`} sizes="32x32"/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-16x16.png`} sizes="16x16"/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-128.png`} sizes="128x128"/>
|
||||
<meta name="application-name" content=" " />
|
||||
<meta name="msapplication-TileColor" content="#1E1E1E" />
|
||||
<meta name="msapplication-TileImage" content="mstile-144x144.png" />
|
||||
<meta name="msapplication-square70x70logo" content="mstile-70x70.png" />
|
||||
<meta name="msapplication-square150x150logo" content="mstile-150x150.png" />
|
||||
<meta name="msapplication-wide310x150logo" content="mstile-310x150.png" />
|
||||
<meta name="msapplication-square310x310logo" content="mstile-310x310.png" />
|
||||
<meta name="theme-color" content="#1E1E1E" />
|
||||
<link rel="shortcut icon" href={`${basePath}/favicon/favicon.ico`} />
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon.ico`} />
|
||||
{/* misc */}
|
||||
<link rel="manifest" href={`${basePath}/favicon/manifest.json`} />
|
||||
<meta name="msapplication-config" content={`${basePath}/favicon/browserconfig.xml`} />
|
||||
<link rel="alternate" type="application/rss+xml" href={`${basePath}/feed.xml`} />
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
|
||||
export default Favicons
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import { useIsLoggedIn } from 'common'
|
||||
import { type Database, useIsLoggedIn } from 'common'
|
||||
import { Button, cn } from 'ui'
|
||||
|
||||
import { useSendFeedbackMutation } from '~/lib/fetch/feedback'
|
||||
import { useSendTelemetryEvent } from '~/lib/telemetry'
|
||||
import { FeedbackModal, type FeedbackFields } from './FeedbackModal'
|
||||
import { getNotionTeam, getSanitizedTabParams } from './Feedback.utils'
|
||||
|
||||
const FeedbackButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
@@ -76,7 +77,7 @@ function Feedback() {
|
||||
const pathname = usePathname()
|
||||
const sendTelemetryEvent = useSendTelemetryEvent()
|
||||
const { mutate: sendFeedbackComment } = useSendFeedbackMutation()
|
||||
const supabase = useSupabaseClient()
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
|
||||
const unanswered = state.type === 'unanswered'
|
||||
const isYes = 'response' in state && state.response === 'yes'
|
||||
@@ -85,7 +86,13 @@ function Feedback() {
|
||||
const showNo = unanswered || isNo
|
||||
|
||||
async function sendFeedbackVote(response: Response) {
|
||||
const { error } = await supabase.from('feedback').insert({ vote: response, page: pathname })
|
||||
const { error } = await supabase.from('feedback').insert({
|
||||
vote: response,
|
||||
page: pathname,
|
||||
metadata: {
|
||||
query: getSanitizedTabParams(),
|
||||
},
|
||||
})
|
||||
if (error) console.error(error)
|
||||
}
|
||||
|
||||
@@ -111,8 +118,15 @@ function Feedback() {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
async function handleSubmit({ page, comment }: FeedbackFields) {
|
||||
sendFeedbackComment({ message: comment, pathname: page })
|
||||
async function handleSubmit({ page, comment, title }: FeedbackFields) {
|
||||
sendFeedbackComment({
|
||||
message: comment,
|
||||
pathname: page,
|
||||
title,
|
||||
// @ts-expect-error -- can't click this button without having a state.response
|
||||
isHelpful: state.response === 'yes',
|
||||
team: getNotionTeam(pathname),
|
||||
})
|
||||
setModalOpen(false)
|
||||
refocusButton()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { pick } from 'lodash'
|
||||
|
||||
/**
|
||||
* Gets the Notion team to send feedback to based on the pathname.
|
||||
*/
|
||||
const getNotionTeam = (pathname: string) => {
|
||||
const DEFAULT_TEAM = 'team-docs'
|
||||
|
||||
// Pathname has format `/guides/(team)/**`
|
||||
const pathParts = pathname.split('/')
|
||||
|
||||
if (pathParts[1] !== 'guides' || !pathParts[2]) return DEFAULT_TEAM
|
||||
|
||||
switch (pathParts[2]) {
|
||||
case 'database':
|
||||
return 'team-postgres'
|
||||
case 'auth':
|
||||
return 'team-auth'
|
||||
case 'storage':
|
||||
return 'team-storage'
|
||||
case 'functions':
|
||||
return 'team-functions'
|
||||
case 'realtime':
|
||||
return 'team-realtime'
|
||||
case 'ai':
|
||||
return 'team-ai'
|
||||
case 'cli':
|
||||
return 'team-cli'
|
||||
|
||||
// Ignoring platform for now because that section is a mix of teams.
|
||||
case 'platform':
|
||||
default:
|
||||
return DEFAULT_TEAM
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tab selection state from the URL search params.
|
||||
*
|
||||
* Sanitizes by including only those search params that are explicitly marked
|
||||
* as query groups.
|
||||
*/
|
||||
const getSanitizedTabParams = () => {
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const queryGroups = searchParams.getAll('queryGroups')
|
||||
|
||||
return pick(Object.fromEntries(searchParams.entries()), queryGroups)
|
||||
}
|
||||
|
||||
export { getNotionTeam, getSanitizedTabParams }
|
||||
@@ -2,6 +2,7 @@ import { Button, Form, Input, Modal } from 'ui'
|
||||
|
||||
export type FeedbackFields = {
|
||||
page: string
|
||||
title: string
|
||||
comment: string
|
||||
}
|
||||
|
||||
@@ -21,6 +22,10 @@ function FeedbackModal({ visible, page, onCancel, onSubmit }: FeedbackModalProps
|
||||
validate={(vals) => {
|
||||
const errors: Partial<FeedbackFields> = {}
|
||||
|
||||
if (!vals.title) {
|
||||
errors.title = 'Required'
|
||||
}
|
||||
|
||||
if (!vals.comment) {
|
||||
errors.comment = 'Required'
|
||||
}
|
||||
@@ -32,8 +37,9 @@ function FeedbackModal({ visible, page, onCancel, onSubmit }: FeedbackModalProps
|
||||
>
|
||||
{({ isSubmitting }: { isSubmitting: boolean }) => (
|
||||
<>
|
||||
<Modal.Content className="pt-4 pb-2">
|
||||
<Modal.Content className="pt-4 pb-2 flex flex-col gap-2">
|
||||
<Input type="hidden" id="page" name="page" value={page} />
|
||||
<Input type="text" id="title" name="title" label="Title" className="mb-2" />
|
||||
<Input.TextArea
|
||||
label="Comment"
|
||||
id="comment"
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { FC } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { cn, TAB_CHANGE_EVENT_NAME } from 'ui'
|
||||
import { ExpandableVideo } from 'ui-patterns'
|
||||
|
||||
import { highlightSelectedTocItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils'
|
||||
import useHash from '~/hooks/useHash'
|
||||
import { useRerenderOnEvt } from '~/hooks/useManualRerender'
|
||||
import { removeAnchor } from './CustomHTMLElements/CustomHTMLElements.utils'
|
||||
import { Feedback } from './Feedback'
|
||||
|
||||
const formatSlug = (slug: string) => {
|
||||
// [Joshen] We will still provide support for headers declared like this:
|
||||
@@ -28,30 +37,91 @@ const formatTOCHeader = (content: string) => {
|
||||
return res.join('')
|
||||
}
|
||||
|
||||
interface TOCHeader {
|
||||
id: number
|
||||
level: number
|
||||
text: string
|
||||
link: string
|
||||
}
|
||||
const GuidesTableOfContents = ({
|
||||
className,
|
||||
overrideToc,
|
||||
video,
|
||||
}: {
|
||||
className?: string
|
||||
overrideToc?: Array<{ text: string; link: string; level: number }>
|
||||
video?: string
|
||||
}) => {
|
||||
const [tocList, setTocList] = useState([])
|
||||
const pathname = usePathname()
|
||||
const [hash] = useHash()
|
||||
|
||||
interface Props {
|
||||
list: TOCHeader[]
|
||||
}
|
||||
const displayedList = overrideToc ?? tocList
|
||||
|
||||
useEffect(() => {
|
||||
if (overrideToc) return
|
||||
|
||||
/**
|
||||
* Because we're directly querying the DOM, needs the setTimeout so the DOM
|
||||
* update will happen first.
|
||||
*/
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
const headings = Array.from(
|
||||
document.querySelector('#sb-docs-guide-main-article')?.querySelectorAll('h2, h3') ?? []
|
||||
)
|
||||
const newHeadings = headings
|
||||
.filter((heading) => heading.id)
|
||||
.map((heading) => {
|
||||
const text = heading.textContent.replace('#', '')
|
||||
const link = heading.querySelector('a').getAttribute('href')
|
||||
const level = heading.tagName === 'H2' ? 2 : 3
|
||||
return { text, link, level }
|
||||
})
|
||||
setTocList(newHeadings)
|
||||
})
|
||||
|
||||
return () => clearTimeout(timeoutHandle)
|
||||
/**
|
||||
* window.location.href needed to recalculate toc when page changes,
|
||||
* useRerenderOnEvt below will guarantee rerender on change
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [overrideToc, typeof window !== 'undefined' && window.location.href])
|
||||
|
||||
useEffect(() => {
|
||||
if (hash && displayedList.length > 0) {
|
||||
highlightSelectedTocItem(hash)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hash, JSON.stringify(displayedList)])
|
||||
|
||||
/**
|
||||
* Displayed headings may change if the tab changes, so the table of contents
|
||||
* needs to rerender.
|
||||
*/
|
||||
useRerenderOnEvt(TAB_CHANGE_EVENT_NAME)
|
||||
|
||||
if (!displayedList.length) return
|
||||
|
||||
const tocVideoPreview = `http://img.youtube.com/vi/${video}/0.jpg`
|
||||
|
||||
const GuidesTableOfContents: FC<Props> = ({ list }) => {
|
||||
return (
|
||||
<ul className="toc-menu list-none pl-5 text-[0.8rem] grid gap-2">
|
||||
{list.map((item, i) => (
|
||||
<li key={`${item.level}-${i}`} className={item.level === 3 ? 'ml-4' : ''}>
|
||||
<a
|
||||
href={`#${formatSlug(item.link)}`}
|
||||
className="text-foreground-lighter hover:text-brand-link transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.text)) }}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={cn('border-l', 'thin-scrollbar overflow-y-auto', 'px-2', className)}>
|
||||
{video && (
|
||||
<div className="relative mb-6 pl-5">
|
||||
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} />
|
||||
</div>
|
||||
)}
|
||||
<Feedback key={pathname} />
|
||||
<span className="block font-mono text-xs uppercase text-foreground px-5 mb-6">
|
||||
On this page
|
||||
</span>
|
||||
<ul className="toc-menu list-none pl-5 text-[0.8rem] grid gap-2">
|
||||
{displayedList.map((item, i) => (
|
||||
<li key={`${item.level}-${i}`} className={item.level === 3 ? 'ml-4' : ''}>
|
||||
<a
|
||||
href={`#${formatSlug(item.link)}`}
|
||||
className="text-foreground-lighter hover:text-brand-link transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.text)) }}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@ The next step requires a callback URL, which looks like this: `https://<project-
|
||||
- Go to your [Supabase Project Dashboard](https://supabase.com/dashboard)
|
||||
- Click on the `Authentication` icon in the left sidebar
|
||||
- Click on [`Providers`](https://supabase.com/dashboard/project/_/auth/providers) under the Configuration section
|
||||
- Click on **{props.provider}** from the accordion list to expand and you'll find your **Redirect URL**, you can click `Copy` to copy it to the clipboard
|
||||
- Click on **{props.provider}** from the accordion list to expand and you'll find your **Callback URL**, you can click `Copy` to copy it to the clipboard
|
||||
@@ -1,111 +0,0 @@
|
||||
If you upload additional profile photos, they'll accumulate
|
||||
in the `avatars` bucket because of their random names with only the latest being referenced
|
||||
from `public.profiles` and the older versions getting orphaned.
|
||||
|
||||
To automatically remove obsolete storage objects, extend the database
|
||||
triggers. Note that it is not sufficient to delete the objects from the
|
||||
`storage.objects` table because that would orphan and leak the actual storage objects in
|
||||
the S3 backend. Instead, invoke the storage API within Postgres via the `http` extension.
|
||||
|
||||
Enable the [http extension for the `extensions` schema](https://supabase.com/dashboard/project/_/database/extensions) in the Dashboard.
|
||||
Then, define the following SQL functions in the SQL Editor to delete
|
||||
storage objects via the API:
|
||||
|
||||
```sql
|
||||
create or replace function delete_storage_object(bucket text, object text, out status int, out content text)
|
||||
returns record
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
project_url text := '<YOURPROJECTURL>';
|
||||
service_role_key text := '<YOURSERVICEROLEKEY>'; -- full access needed
|
||||
url text := project_url||'/storage/v1/object/'||bucket||'/'||object;
|
||||
begin
|
||||
select
|
||||
into status, content
|
||||
result.status::int, result.content::text
|
||||
FROM extensions.http((
|
||||
'DELETE',
|
||||
url,
|
||||
ARRAY[extensions.http_header('authorization','Bearer '||service_role_key)],
|
||||
NULL,
|
||||
NULL)::extensions.http_request) as result;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function delete_avatar(avatar_url text, out status int, out content text)
|
||||
returns record
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
select
|
||||
into status, content
|
||||
result.status, result.content
|
||||
from public.delete_storage_object('avatars', avatar_url) as result;
|
||||
end;
|
||||
$$;
|
||||
|
||||
```
|
||||
|
||||
Next, add a trigger that removes any obsolete avatar whenever the
|
||||
profile is updated or deleted:
|
||||
|
||||
```sql
|
||||
create or replace function delete_old_avatar()
|
||||
returns trigger
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
status int;
|
||||
content text;
|
||||
avatar_name text;
|
||||
begin
|
||||
if coalesce(old.avatar_url, '') <> ''
|
||||
and (tg_op = 'DELETE' or (old.avatar_url <> coalesce(new.avatar_url, ''))) then
|
||||
-- extract avatar name
|
||||
avatar_name := old.avatar_url;
|
||||
select
|
||||
into status, content
|
||||
result.status, result.content
|
||||
from public.delete_avatar(avatar_name) as result;
|
||||
if status <> 200 then
|
||||
raise warning 'Could not delete avatar: % %', status, content;
|
||||
end if;
|
||||
end if;
|
||||
if tg_op = 'DELETE' then
|
||||
return old;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger before_profile_changes
|
||||
before update of avatar_url or delete on public.profiles
|
||||
for each row execute function public.delete_old_avatar();
|
||||
|
||||
```
|
||||
|
||||
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
|
||||
create or replace function delete_old_profile()
|
||||
returns trigger
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
delete from public.profiles where id = old.id;
|
||||
return old;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger before_delete_user
|
||||
before delete on auth.users
|
||||
for each row execute function public.delete_old_profile();
|
||||
|
||||
```
|
||||
@@ -19,10 +19,10 @@ create policy "Public profiles are viewable by everyone." on profiles
|
||||
for select using (true);
|
||||
|
||||
create policy "Users can insert their own profile." on profiles
|
||||
for insert with check (auth.uid() = id);
|
||||
for insert with check ((select auth.uid()) = id);
|
||||
|
||||
create policy "Users can update own profile." on profiles
|
||||
for update using (auth.uid() = id);
|
||||
for update using ((select auth.uid()) = id);
|
||||
|
||||
-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
|
||||
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
|
||||
@@ -51,5 +51,5 @@ create policy "Anyone can upload an avatar." on storage.objects
|
||||
for insert with check (bucket_id = 'avatars');
|
||||
|
||||
create policy "Anyone can update their own avatar." on storage.objects
|
||||
for update using (auth.uid() = owner) with check (bucket_id = 'avatars');
|
||||
for update using ((select auth.uid()) = owner) with check (bucket_id = 'avatars');
|
||||
```
|
||||
@@ -40,11 +40,7 @@ const NavigationMenuHome = () => {
|
||||
>
|
||||
{link?.icon && <HomeMenuIconPicker icon={link.icon} />}
|
||||
{link.label}
|
||||
{link.community && (
|
||||
<Badge size="small" color="scale">
|
||||
Community
|
||||
</Badge>
|
||||
)}
|
||||
{link.community && <Badge size="small">Community</Badge>}
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -26,58 +26,58 @@ import {
|
||||
IconMenuDevCli,
|
||||
} from './HomeMenuIcons'
|
||||
|
||||
function getMenuIcon(menuKey: string, width: number = 16, height: number = 16) {
|
||||
function getMenuIcon(menuKey: string, width: number = 16, height: number = 16, className) {
|
||||
switch (menuKey) {
|
||||
case 'home':
|
||||
return <IconMenuHome width={width} height={height} />
|
||||
return <IconMenuHome width={width} height={height} className={className} />
|
||||
case 'getting-started':
|
||||
return <IconMenuGettingStarted width={width} height={height} />
|
||||
return <IconMenuGettingStarted width={width} height={height} className={className} />
|
||||
case 'database':
|
||||
return <IconMenuDatabase width={width} height={height} />
|
||||
return <IconMenuDatabase width={width} height={height} className={className} />
|
||||
case 'rest':
|
||||
return <IconMenuRestApis width={width} height={height} />
|
||||
return <IconMenuRestApis width={width} height={height} className={className} />
|
||||
case 'graphql':
|
||||
return <IconMenuGraphQL width={width} height={height} />
|
||||
return <IconMenuGraphQL width={width} height={height} className={className} />
|
||||
case 'auth':
|
||||
return <IconMenuAuth width={width} height={height} />
|
||||
return <IconMenuAuth width={width} height={height} className={className} />
|
||||
case 'edge-functions':
|
||||
return <IconMenuEdgeFunctions width={width} height={height} />
|
||||
return <IconMenuEdgeFunctions width={width} height={height} className={className} />
|
||||
case 'realtime':
|
||||
return <IconMenuRealtime width={width} height={height} />
|
||||
return <IconMenuRealtime width={width} height={height} className={className} />
|
||||
case 'storage':
|
||||
return <IconMenuStorage width={width} height={height} />
|
||||
return <IconMenuStorage width={width} height={height} className={className} />
|
||||
case 'ai':
|
||||
return <IconMenuAI width={width} height={height} />
|
||||
return <IconMenuAI width={width} height={height} className={className} />
|
||||
case 'platform':
|
||||
return <IconMenuPlatform width={width} height={height} />
|
||||
return <IconMenuPlatform width={width} height={height} className={className} />
|
||||
case 'resources':
|
||||
return <IconMenuResources width={width} height={height} />
|
||||
return <IconMenuResources width={width} height={height} className={className} />
|
||||
case 'self-hosting':
|
||||
return <IconMenuSelfHosting width={width} height={height} />
|
||||
return <IconMenuSelfHosting width={width} height={height} className={className} />
|
||||
case 'integrations':
|
||||
return <IconMenuIntegrations width={width} height={height} />
|
||||
return <IconMenuIntegrations width={width} height={height} className={className} />
|
||||
case 'reference-javascript':
|
||||
return <IconMenuJavascript width={width} height={height} />
|
||||
return <IconMenuJavascript width={width} height={height} className={className} />
|
||||
case 'reference-dart':
|
||||
return <IconMenuFlutter width={width} height={height} />
|
||||
return <IconMenuFlutter width={width} height={height} className={className} />
|
||||
case 'reference-python':
|
||||
return <IconMenuPython width={width} height={height} />
|
||||
return <IconMenuPython width={width} height={height} className={className} />
|
||||
case 'reference-csharp':
|
||||
return <IconMenuCsharp width={width} height={height} />
|
||||
return <IconMenuCsharp width={width} height={height} className={className} />
|
||||
case 'reference-swift':
|
||||
return <IconMenuSwift width={width} height={height} />
|
||||
return <IconMenuSwift width={width} height={height} className={className} />
|
||||
case 'reference-kotlin':
|
||||
return <IconMenuKotlin width={width} height={height} />
|
||||
return <IconMenuKotlin width={width} height={height} className={className} />
|
||||
case 'reference-api':
|
||||
return <IconMenuApi width={width} height={height} />
|
||||
return <IconMenuApi width={width} height={height} className={className} />
|
||||
case 'dev-cli':
|
||||
return <IconMenuDevCli width={width} height={height} />
|
||||
return <IconMenuDevCli width={width} height={height} className={className} />
|
||||
case 'reference-cli':
|
||||
return <IconMenuCli width={width} height={height} />
|
||||
return <IconMenuCli width={width} height={height} className={className} />
|
||||
case 'status':
|
||||
return <IconMenuStatus width={width} height={height} />
|
||||
return <IconMenuStatus width={width} height={height} className={className} />
|
||||
default:
|
||||
return <IconMenuPlatform width={width} height={height} />
|
||||
return <IconMenuPlatform width={width} height={height} className={className} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,12 +85,14 @@ type HomeMenuIconPickerProps = {
|
||||
icon: string
|
||||
width?: number
|
||||
height?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function HomeMenuIconPicker({
|
||||
icon,
|
||||
width = 16,
|
||||
height = 16,
|
||||
className,
|
||||
}: HomeMenuIconPickerProps) {
|
||||
return getMenuIcon(icon, width, height)
|
||||
return getMenuIcon(icon, width, height, className)
|
||||
}
|
||||
@@ -3,11 +3,13 @@ import { products } from 'shared-data'
|
||||
type HomeMenuIcon = {
|
||||
width?: number
|
||||
height?: number
|
||||
className?: string
|
||||
}
|
||||
export function IconMenuHome({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuHome({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -23,10 +25,11 @@ export function IconMenuHome({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuGraphQL({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuGraphQL({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -42,10 +45,11 @@ export function IconMenuGraphQL({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuApi({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuApi({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -77,9 +81,10 @@ export function IconMenuApi({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuAuth({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuAuth({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -96,10 +101,11 @@ export function IconMenuAuth({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuDevCli({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuDevCli({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -115,10 +121,11 @@ export function IconMenuDevCli({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuCli({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuCli({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -132,9 +139,10 @@ export function IconMenuCli({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuCsharp({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuCsharp({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -151,10 +159,11 @@ export function IconMenuCsharp({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuDatabase({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuDatabase({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -170,9 +179,10 @@ export function IconMenuDatabase({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuRestApis({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuRestApis({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -189,10 +199,11 @@ export function IconMenuRestApis({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuEdgeFunctions({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuEdgeFunctions({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -208,9 +219,10 @@ export function IconMenuEdgeFunctions({ width = 16, height = 16 }: HomeMenuIcon)
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuExtensions({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuExtensions({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -227,9 +239,10 @@ export function IconMenuExtensions({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuFlutter({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuFlutter({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -246,10 +259,11 @@ export function IconMenuFlutter({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuGettingStarted({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuGettingStarted({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -265,10 +279,11 @@ export function IconMenuGettingStarted({ width = 16, height = 16 }: HomeMenuIcon
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuIntegrations({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuIntegrations({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -284,9 +299,10 @@ export function IconMenuIntegrations({ width = 16, height = 16 }: HomeMenuIcon)
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuJavascript({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuJavascript({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
fill="none"
|
||||
@@ -303,9 +319,10 @@ export function IconMenuJavascript({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuPlatform({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuPlatform({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -322,9 +339,10 @@ export function IconMenuPlatform({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuPython({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuPython({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -341,10 +359,11 @@ export function IconMenuPython({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuRealtime({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuRealtime({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -360,10 +379,11 @@ export function IconMenuRealtime({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuResources({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuResources({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -379,9 +399,10 @@ export function IconMenuResources({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuSelfHosting({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuSelfHosting({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -398,9 +419,10 @@ export function IconMenuSelfHosting({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuStorage({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuStorage({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -417,9 +439,10 @@ export function IconMenuStorage({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuAI({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuAI({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -437,9 +460,10 @@ export function IconMenuAI({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuSwift({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuSwift({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -456,10 +480,11 @@ export function IconMenuSwift({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuKotlin({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuKotlin({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -475,9 +500,10 @@ export function IconMenuKotlin({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuStatus({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuStatus({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 18 18"
|
||||
|
||||
@@ -112,6 +112,12 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
|
||||
href: '/reference/dart/introduction',
|
||||
level: 'reference_dart',
|
||||
},
|
||||
{
|
||||
label: 'Swift',
|
||||
icon: 'reference-swift',
|
||||
href: '/reference/swift/introduction',
|
||||
level: 'reference_swift',
|
||||
},
|
||||
{
|
||||
label: 'Python',
|
||||
icon: 'reference-python',
|
||||
@@ -126,13 +132,6 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
|
||||
level: 'reference_csharp',
|
||||
community: true,
|
||||
},
|
||||
{
|
||||
label: 'Swift',
|
||||
icon: 'reference-swift',
|
||||
href: '/reference/swift/introduction',
|
||||
level: 'reference_swift',
|
||||
community: true,
|
||||
},
|
||||
{
|
||||
label: 'Kotlin',
|
||||
icon: 'reference-kotlin',
|
||||
@@ -528,6 +527,7 @@ export const auth = {
|
||||
name: 'Authentication',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Anonymous Sign-Ins', url: '/guides/auth/auth-anonymous' },
|
||||
{ name: 'Email Login', url: '/guides/auth/auth-email' },
|
||||
{
|
||||
name: 'Passwordless Login',
|
||||
@@ -592,7 +592,7 @@ export const auth = {
|
||||
items: [
|
||||
{
|
||||
name: 'SAML 2.0',
|
||||
url: '/guides/auth/sso/auth-sso-saml',
|
||||
url: '/guides/auth/enterprise-sso/auth-sso-saml',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -609,10 +609,6 @@ export const auth = {
|
||||
{ name: 'Managing User Data', url: '/guides/auth/managing-user-data' },
|
||||
{ name: 'Multi-Factor Authentication', url: '/guides/auth/auth-mfa' },
|
||||
{ name: 'Row Level Security', url: '/guides/auth/row-level-security' },
|
||||
{
|
||||
name: 'Column Level Security',
|
||||
url: '/guides/auth/column-level-security',
|
||||
},
|
||||
{
|
||||
name: 'Custom Claims & RBAC',
|
||||
url: '/guides/auth/custom-claims-and-role-based-access-control-rbac',
|
||||
@@ -628,6 +624,10 @@ export const auth = {
|
||||
name: 'Next.js guide',
|
||||
url: '/guides/auth/server-side/nextjs',
|
||||
},
|
||||
{
|
||||
name: 'SvelteKit guide',
|
||||
url: '/guides/auth/server-side/sveltekit',
|
||||
},
|
||||
{
|
||||
name: 'Creating a client',
|
||||
url: '/guides/auth/server-side/creating-a-client',
|
||||
@@ -700,6 +700,12 @@ export const database: NavMenuConstant = {
|
||||
{
|
||||
name: 'Connecting to your database',
|
||||
url: '/guides/database/connecting-to-postgres',
|
||||
items: [
|
||||
{
|
||||
name: 'Serverless Drivers',
|
||||
url: '/guides/database/connecting-to-postgres/serverless-drivers',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: 'Importing data', url: '/guides/database/import-data' },
|
||||
{ name: 'Securing your data', url: '/guides/database/secure-data' },
|
||||
@@ -769,6 +775,10 @@ export const database: NavMenuConstant = {
|
||||
name: 'Superuser Access and Unsupported Operations',
|
||||
url: '/guides/database/postgres/roles-superuser',
|
||||
},
|
||||
{
|
||||
name: 'Column level privileges',
|
||||
url: '/guides/database/postgres/column-level-security',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -787,7 +797,14 @@ export const database: NavMenuConstant = {
|
||||
name: 'Query optimization',
|
||||
url: '/guides/database/query-optimization',
|
||||
},
|
||||
{ name: 'Debugging and monitoring', url: '/guides/database/inspect' },
|
||||
{
|
||||
name: 'Database linter',
|
||||
url: '/guides/database/database-linter',
|
||||
},
|
||||
{
|
||||
name: 'Debugging and monitoring',
|
||||
url: '/guides/database/inspect',
|
||||
},
|
||||
{
|
||||
name: 'Debugging performance issues',
|
||||
url: '/guides/database/debugging-performance',
|
||||
@@ -910,10 +927,18 @@ export const database: NavMenuConstant = {
|
||||
name: 'Overview',
|
||||
url: '/guides/database/extensions/wrappers/overview',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to Auth0',
|
||||
url: '/guides/database/extensions/wrappers/auth0',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to Airtable',
|
||||
url: '/guides/database/extensions/wrappers/airtable',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to AWS Cognito',
|
||||
url: '/guides/database/extensions/wrappers/cognito',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to AWS S3',
|
||||
url: '/guides/database/extensions/wrappers/s3',
|
||||
@@ -934,6 +959,14 @@ export const database: NavMenuConstant = {
|
||||
name: 'Connecting to Logflare',
|
||||
url: '/guides/database/extensions/wrappers/logflare',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to MSSQL',
|
||||
url: '/guides/database/extensions/wrappers/mssql',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to Redis',
|
||||
url: '/guides/database/extensions/wrappers/redis',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to Stripe',
|
||||
url: '/guides/database/extensions/wrappers/stripe',
|
||||
@@ -1110,6 +1143,10 @@ export const functions: NavMenuConstant = {
|
||||
name: 'Handling Routing in Functions',
|
||||
url: '/guides/functions/routing',
|
||||
},
|
||||
{
|
||||
name: 'Running AI Models',
|
||||
url: '/guides/functions/ai-models',
|
||||
},
|
||||
{
|
||||
name: 'Regional invocations',
|
||||
url: '/guides/functions/regional-invocation',
|
||||
@@ -1158,6 +1195,10 @@ export const functions: NavMenuConstant = {
|
||||
name: 'Generating OG images ',
|
||||
url: '/guides/functions/examples/og-image',
|
||||
},
|
||||
{
|
||||
name: 'Semantic AI Search',
|
||||
url: '/guides/functions/examples/semantic-search',
|
||||
},
|
||||
{
|
||||
name: 'CAPTCHA support with Cloudflare Turnstile',
|
||||
url: '/guides/functions/examples/cloudflare-turnstile',
|
||||
@@ -1252,10 +1293,6 @@ export const realtime: NavMenuConstant = {
|
||||
name: 'Guides',
|
||||
url: undefined,
|
||||
items: [
|
||||
{
|
||||
name: 'Throttling messages',
|
||||
url: '/guides/realtime/guides/client-side-throttling',
|
||||
},
|
||||
{
|
||||
name: 'Subscribing to Database Changes',
|
||||
url: '/guides/realtime/subscribing-to-database-changes',
|
||||
@@ -1305,6 +1342,10 @@ export const storage: NavMenuConstant = {
|
||||
name: 'Security',
|
||||
url: undefined,
|
||||
items: [
|
||||
{
|
||||
name: 'Ownership',
|
||||
url: '/guides/storage/security/ownership',
|
||||
},
|
||||
{
|
||||
name: 'Access Control',
|
||||
url: '/guides/storage/security/access-control',
|
||||
@@ -1323,6 +1364,10 @@ export const storage: NavMenuConstant = {
|
||||
name: 'Resumable Uploads',
|
||||
url: '/guides/storage/uploads/resumable-uploads',
|
||||
},
|
||||
{
|
||||
name: 'S3 Uploads',
|
||||
url: '/guides/storage/uploads/s3-uploads',
|
||||
},
|
||||
{ name: 'Limits', url: '/guides/storage/uploads/file-limits' },
|
||||
],
|
||||
},
|
||||
@@ -1335,6 +1380,26 @@ export const storage: NavMenuConstant = {
|
||||
name: 'Image Transformations',
|
||||
url: '/guides/storage/serving/image-transformations',
|
||||
},
|
||||
{
|
||||
name: 'Bandwidth & Storage Egress',
|
||||
url: '/guides/storage/serving/bandwidth',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Management',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Copy / Move Objects', url: '/guides/storage/management/copy-move-objects' },
|
||||
{ name: 'Delete Objects', url: '/guides/storage/management/delete-objects' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'S3',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Authentication', url: '/guides/storage/s3/authentication' },
|
||||
{ name: 'API Compatibility', url: '/guides/storage/s3/compatibility' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1363,6 +1428,7 @@ export const storage: NavMenuConstant = {
|
||||
name: 'Helper Functions',
|
||||
url: '/guides/storage/schema/helper-functions',
|
||||
},
|
||||
{ name: 'Custom Roles', url: '/guides/storage/schema/custom-roles' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1405,7 +1471,6 @@ export const ai = {
|
||||
url: '/guides/ai/vector-indexes',
|
||||
items: vectorIndexItems,
|
||||
},
|
||||
{ name: 'Hybrid search', url: '/guides/ai/hybrid-search' },
|
||||
{
|
||||
name: 'Engineering for scale',
|
||||
url: '/guides/ai/engineering-for-scale',
|
||||
@@ -1421,6 +1486,15 @@ export const ai = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Semantic search', url: '/guides/ai/semantic-search' },
|
||||
{ name: 'Keyword search', url: '/guides/ai/keyword-search' },
|
||||
{ name: 'Hybrid search', url: '/guides/ai/hybrid-search' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'JavaScript Examples',
|
||||
url: undefined,
|
||||
@@ -1485,6 +1559,10 @@ export const ai = {
|
||||
name: 'Image search with OpenAI CLIP',
|
||||
url: '/guides/ai/examples/image-search-openai-clip',
|
||||
},
|
||||
{
|
||||
name: 'Semantic search with Amazon Titan',
|
||||
url: '/guides/ai/examples/semantic-image-search-amazon-titan',
|
||||
},
|
||||
{
|
||||
name: 'Building ChatGPT Plugins',
|
||||
url: '/guides/ai/examples/building-chatgpt-plugins',
|
||||
@@ -1515,6 +1593,10 @@ export const ai = {
|
||||
name: 'Roboflow',
|
||||
url: '/guides/ai/integrations/roboflow',
|
||||
},
|
||||
{
|
||||
name: 'Amazon Bedrock',
|
||||
url: '/guides/ai/integrations/amazon-bedrock',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -1756,6 +1838,10 @@ export const platform: NavMenuConstant = {
|
||||
name: 'High RAM Usage',
|
||||
url: '/guides/platform/exhaust-ram',
|
||||
},
|
||||
{
|
||||
name: 'High Swap Usage',
|
||||
url: '/guides/platform/exhaust-swap',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,6 +2,10 @@ import { useEffect, useState } from 'react'
|
||||
import { ICommonItem } from '~/components/reference/Reference.types'
|
||||
import { Json } from '~/types'
|
||||
|
||||
export function getPathWithoutHash(relativePath: string) {
|
||||
return new URL(relativePath, 'http://placeholder').pathname
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively filter common sections and their sub items based on
|
||||
* what is available in their spec
|
||||
|
||||
@@ -1,39 +1,23 @@
|
||||
import * as Accordion from '@radix-ui/react-accordion'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import NavigationMenuGuideListItems from './NavigationMenuGuideListItems'
|
||||
import * as NavItems from './NavigationMenu.constants'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
collapsible?: boolean
|
||||
value?: string[]
|
||||
}
|
||||
const NavigationMenuGuideList: React.FC<Props> = ({ id, value }) => {
|
||||
const router = useRouter()
|
||||
import * as NavItems from './NavigationMenu.constants'
|
||||
import { getPathWithoutHash } from './NavigationMenu.utils'
|
||||
import NavigationMenuGuideListItems from './NavigationMenuGuideListItems'
|
||||
|
||||
const NavigationMenuGuideList = ({ id }: { id: string }) => {
|
||||
const path = useRouter().asPath
|
||||
const url = getPathWithoutHash(path)
|
||||
const firstLevelRoute = url?.split('/')?.slice(0, 4)?.join('/')
|
||||
|
||||
const menu = NavItems[id]
|
||||
|
||||
// get url
|
||||
const url = router.asPath
|
||||
|
||||
// We need to decide how deep we want the menu to be for matching urls
|
||||
// if the links are really deep, we don't want to match all the way out
|
||||
// But we need to reach out further to make the structure of /resources/postgres/ work
|
||||
// look at /resources/postgres/ vs /auth/phone-login for how these are different
|
||||
let firstLevelRoute
|
||||
if (url.includes('resources/postgres/')) {
|
||||
firstLevelRoute = url?.split('/')?.slice(0, 5)?.join('/')
|
||||
} else {
|
||||
firstLevelRoute = url?.split('/')?.slice(0, 4)?.join('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion.Root
|
||||
collapsible={true}
|
||||
key={id}
|
||||
type={value ? 'multiple' : 'single'}
|
||||
value={value ?? firstLevelRoute}
|
||||
type="single"
|
||||
value={firstLevelRoute}
|
||||
className="transition-all duration-150 ease-out opacity-100 ml-0 delay-150"
|
||||
>
|
||||
<NavigationMenuGuideListItems menu={menu} id={id} />
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import * as Accordion from '@radix-ui/react-accordion'
|
||||
import { ChevronLeft } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import Image from 'next/legacy/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { IconChevronLeft } from '~/../../packages/ui'
|
||||
import * as Accordion from '@radix-ui/react-accordion'
|
||||
|
||||
import HomeMenuIconPicker from './HomeMenuIconPicker'
|
||||
import { getPathWithoutHash } from './NavigationMenu.utils'
|
||||
|
||||
const HeaderLink = React.memo(function HeaderLink(props: {
|
||||
title: string
|
||||
@@ -30,7 +32,7 @@ const HeaderLink = React.memo(function HeaderLink(props: {
|
||||
const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any) {
|
||||
const router = useRouter()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const activeItem = props.subItem.url === router.asPath
|
||||
const activeItem = props.subItem.url === getPathWithoutHash(router.asPath)
|
||||
const activeItemRef = useRef(null)
|
||||
|
||||
const LinkContainer = (props) => {
|
||||
@@ -97,7 +99,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
|
||||
href={`${subSubItem.url}`}
|
||||
className={[
|
||||
'cursor-pointer transition text-sm',
|
||||
subSubItem.url === router.asPath
|
||||
subSubItem.url === getPathWithoutHash(router.asPath)
|
||||
? 'text-brand'
|
||||
: 'hover:text-brand text-foreground-lighter',
|
||||
].join(' ')}
|
||||
@@ -151,7 +153,7 @@ const Content = (props) => {
|
||||
>
|
||||
<div className="relative w-2">
|
||||
<div className="transition-all ease-out ml-0 group-hover:-ml-1">
|
||||
<IconChevronLeft size={10} strokeWidth={3} />
|
||||
<ChevronLeft size={10} strokeWidth={3} />
|
||||
</div>
|
||||
</div>
|
||||
<span>Back to Home</span>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ReactMarkdown } from 'react-markdown/lib/react-markdown'
|
||||
|
||||
type IParamProps = any
|
||||
|
||||
const Param = ({ name, isOptional, type, description, children }: IParamProps) => {
|
||||
@@ -18,7 +20,9 @@ const Param = ({ name, isOptional, type, description, children }: IParamProps) =
|
||||
</span>
|
||||
<span className="text-foreground-muted text-xs">{type ?? 'no type'}</span>
|
||||
</div>
|
||||
{description && <p className="text-sm text-foreground-lighter m-0">{description} </p>}
|
||||
{description && (
|
||||
<ReactMarkdown className="text-sm text-foreground-lighter m-0">{description}</ReactMarkdown>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
// [Terry]
|
||||
// Delete this after we've implemented GuidesTableofContents and moved all guides
|
||||
// and rename GuidesTableofContents to TableOfContents
|
||||
|
||||
import { FC } from 'react'
|
||||
import { getAnchor, removeAnchor } from './CustomHTMLElements/CustomHTMLElements.utils'
|
||||
|
||||
interface TOCHeader {
|
||||
id: number
|
||||
lvl: number
|
||||
seen: number
|
||||
content: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
toc: any
|
||||
video?: string
|
||||
}
|
||||
|
||||
const formatSlug = (slug: string) => {
|
||||
// [Joshen] We will still provide support for headers declared like this:
|
||||
// ## REST API {#rest-api-overview}
|
||||
// At least for now, this was a docusaurus thing.
|
||||
if (slug.includes('#')) return slug.split('-#')[1]
|
||||
return slug
|
||||
}
|
||||
|
||||
const formatTOCHeader = (content: string) => {
|
||||
let begin = false
|
||||
const res = []
|
||||
for (const x of content) {
|
||||
if (x === '`') {
|
||||
if (!begin) {
|
||||
begin = true
|
||||
res.push(`<code class="text-xs border rounded bg-surface-200 border-overlay">`)
|
||||
} else {
|
||||
begin = false
|
||||
res.push(`</code>`)
|
||||
}
|
||||
} else {
|
||||
res.push(x)
|
||||
}
|
||||
}
|
||||
return res.join('')
|
||||
}
|
||||
|
||||
const TableOfContents: FC<Props> = ({ toc, video }) => {
|
||||
// [Joshen] markdown-toc doesn't seem to read maxdepth from the options passed in
|
||||
// Our first level headers will be H2s (H1 is ignored), and we only show up to H3
|
||||
|
||||
return (
|
||||
<>
|
||||
{video && (
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/0Fs96oZ4se0"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
)}
|
||||
<ul className="toc-menu list-none pl-4 text-[0.8rem] grid gap-2 mt-12">
|
||||
{(toc.json as TOCHeader[])
|
||||
.filter((item) => item.lvl !== 1 && item.lvl <= 3)
|
||||
.map((item: any, i: number) => {
|
||||
return (
|
||||
<li key={i} id={item.lvl} style={{ marginLeft: `${(item.lvl - 2) * 1}rem` }}>
|
||||
<a
|
||||
href={`#${formatSlug(item.slug)}`}
|
||||
className="text-foreground-lighter hover:text-brand transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.content)) }}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TableOfContents
|
||||
@@ -41,7 +41,6 @@ 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'
|
||||
|
||||
// Icons
|
||||
import {
|
||||
@@ -153,7 +152,6 @@ const components = {
|
||||
SocialProviderSettingsSupabase,
|
||||
SocialProviderSetup,
|
||||
StepHikeCompact,
|
||||
StorageManagement,
|
||||
TabPanel: (props: any) => <Tabs.Panel {...props}>{props.children}</Tabs.Panel>,
|
||||
Tabs: (props: any) => <Tabs wrappable {...props} />,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CodeBlock, Tabs } from 'ui'
|
||||
import Param from '~/components/Params'
|
||||
import RefSubLayout from '~/layouts/ref/RefSubLayout'
|
||||
import { ReactMarkdown } from 'react-markdown/lib/react-markdown'
|
||||
|
||||
const ApiOperationSection = (props) => {
|
||||
const operation = props.spec.operations.find((x: any) => x.operationId === props.funcData.id)
|
||||
@@ -96,7 +97,7 @@ const ApiOperationSection = (props) => {
|
||||
>
|
||||
{operation.responseList.map((response: any, i: number) => (
|
||||
<Tabs.Panel key={i} id={response.responseCode} label={response.responseCode}>
|
||||
<p className="text-foreground">{response.description}</p>
|
||||
<ReactMarkdown className="text-foreground">{response.description}</ReactMarkdown>
|
||||
{response?.content && response?.content['application/json'] && (
|
||||
<div className="mt-8">
|
||||
<CodeBlock language="bash" className="relative">
|
||||
|
||||
@@ -23,7 +23,7 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
|
||||
const tsDefinition =
|
||||
hasTsRef && props.typeSpec ? extractTsDocNode(hasTsRef, props.typeSpec) : null
|
||||
const parameters = hasTsRef && tsDefinition ? generateParameters(tsDefinition) : item.params
|
||||
const shortText = hasTsRef && tsDefinition ? tsDefinition.signatures[0].comment.shortText : ''
|
||||
const shortText = hasTsRef && tsDefinition ? tsDefinition.signatures[0].comment?.shortText : ''
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -16,6 +16,14 @@ The toolkit includes:
|
||||
- [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.
|
||||
|
||||
## Search
|
||||
|
||||
You can use Supabase to build different types of search features for your app, including:
|
||||
|
||||
- [Semantic search](/docs/guides/ai/semantic-search): search by meaning rather than exact keywords
|
||||
- [Keyword search](/docs/guides/ai/keyword-search): search by words or phrases
|
||||
- [Hybrid search](/docs/guides/ai/hybrid-search): combine semantic search with keyword search
|
||||
|
||||
## Examples
|
||||
|
||||
Check out all of the AI [templates and examples](https://github.com/supabase/supabase/tree/master/examples/ai) in our GitHub repository.
|
||||
@@ -91,6 +99,12 @@ export const integrations = [
|
||||
'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: 'Amazon Bedrock',
|
||||
description:
|
||||
'A fully managed service that offers a choice of high-performing foundation models from leading AI companies.',
|
||||
href: '/guides/ai/amazon-bedrock',
|
||||
},
|
||||
{
|
||||
name: 'Hugging Face',
|
||||
description:
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: supabase/supabase-embeddings-generator@v0.0.x # Update this to the latest version.
|
||||
- uses: 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 }}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
---
|
||||
id: 'semantic-image-search-python-amazon-titan'
|
||||
title: 'Semantic Image Search with Amazon Titan'
|
||||
description: 'Implement semantic image search with Amazon Titan and Supabase Vector in Python.'
|
||||
subtitle: 'Implement semantic image search with Amazon Titan and Supabase Vector in Python.'
|
||||
tocVideo: 'A3uND5sgiO0'
|
||||
---
|
||||
|
||||
[Amazon Bedrock](https://aws.amazon.com/bedrock) is a fully managed service that offers a choice of high-performing foundation models (FMs) from leading AI companies like AI21 Labs, Anthropic, Cohere, Meta, Mistral AI, Stability AI, and Amazon. Each model is accessible through a common API which implements a broad set of features to help build generative AI applications with security, privacy, and responsible AI in mind.
|
||||
|
||||
[Amazon Titan](https://aws.amazon.com/bedrock/titan/) is a family of foundation models (FMs) for text and image generation, summarization, classification, open-ended Q&A, information extraction, and text or image search.
|
||||
|
||||
In this guide we'll look at how we can get started with Amazon Bedrock and Supabase Vector in Python using the Amazon Titan multimodal model and the [vecs client](/docs/guides/ai/vecs-python-client).
|
||||
|
||||
You can find the full application code as a Python Poetry project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/aws_bedrock_image_search).
|
||||
|
||||
## 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 aws_bedrock_image_search
|
||||
```
|
||||
|
||||
## Spin up a Postgres Database with pgvector
|
||||
|
||||
If you haven't already, head over to [database.new](https://database.new) and create a new project. Every Supabase project comes with a full Postgres database and the [pgvector extension](/docs/guides/database/extensions/pgvector) preconfigured.
|
||||
|
||||
When creating your project, make sure to note down your database password as you will need it to construct the `DB_URL` in the next step.
|
||||
|
||||
You can find the database connection string in your Supabase Dashboard [database settings](https://supabase.com/dashboard/project/_/settings/database). Select "Use connection pooling" with `Mode: Session` for a direct connection to your Postgres database. It will look something like this:
|
||||
|
||||
```txt
|
||||
postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres
|
||||
```
|
||||
|
||||
## 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.
|
||||
- [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html): AWS SDK for Python.
|
||||
- [`matplotlib`](https://matplotlib.org/): for displaying our image result.
|
||||
|
||||
```shell
|
||||
poetry add vecs boto3 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
|
||||
import sys
|
||||
import boto3
|
||||
import vecs
|
||||
import json
|
||||
import base64
|
||||
from matplotlib import pyplot as plt
|
||||
from matplotlib import image as mpimg
|
||||
from typing import Optional
|
||||
|
||||
DB_CONNECTION = "postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres"
|
||||
```
|
||||
|
||||
Next, get the [credentials to your AWS account](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) and instantiate the `boto3` client:
|
||||
|
||||
```python
|
||||
bedrock_client = boto3.client(
|
||||
'bedrock-runtime',
|
||||
region_name='us-west-2',
|
||||
# Credentials from your AWS account
|
||||
aws_access_key_id='<replace_your_own_credentials>',
|
||||
aws_secret_access_key='<replace_your_own_credentials>',
|
||||
aws_session_token='<replace_your_own_credentials>',
|
||||
)
|
||||
```
|
||||
|
||||
## Create embeddings for your images
|
||||
|
||||
In the root of your project, create a new folder called `images` and add some images. You can use the images from the example project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/aws_bedrock_image_search/images) or you can find license free images on [unsplash](https://unsplash.com).
|
||||
|
||||
To send images to the Amazon Bedrock API we need to need to encode them as `base64` strings. Create the following helper methods:
|
||||
|
||||
```python
|
||||
def readFileAsBase64(file_path):
|
||||
"""Encode image as base64 string."""
|
||||
try:
|
||||
with open(file_path, "rb") as image_file:
|
||||
input_image = base64.b64encode(image_file.read()).decode("utf8")
|
||||
return input_image
|
||||
except:
|
||||
print("bad file name")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def construct_bedrock_image_body(base64_string):
|
||||
"""Construct the request body.
|
||||
|
||||
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-embed-mm.html
|
||||
"""
|
||||
return json.dumps(
|
||||
{
|
||||
"inputImage": base64_string,
|
||||
"embeddingConfig": {"outputEmbeddingLength": 1024},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_embedding_from_titan_multimodal(body):
|
||||
"""Invoke the Amazon Titan Model via API request."""
|
||||
response = bedrock_client.invoke_model(
|
||||
body=body,
|
||||
modelId="amazon.titan-embed-image-v1",
|
||||
accept="application/json",
|
||||
contentType="application/json",
|
||||
)
|
||||
|
||||
response_body = json.loads(response.get("body").read())
|
||||
print(response_body)
|
||||
return response_body["embedding"]
|
||||
|
||||
|
||||
def encode_image(file_path):
|
||||
"""Generate embedding for the image at file_path."""
|
||||
base64_string = readFileAsBase64(file_path)
|
||||
body = construct_bedrock_image_body(base64_string)
|
||||
emb = get_embedding_from_titan_multimodal(body)
|
||||
return emb
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
# get or create a collection of vectors with 1024 dimensions
|
||||
images = vx.get_or_create_collection(name="image_vectors", dimension=1024)
|
||||
|
||||
# Generate image embeddings with Amazon Titan Model
|
||||
img_emb1 = encode_image('./images/one.jpg')
|
||||
img_emb2 = encode_image('./images/two.jpg')
|
||||
img_emb3 = encode_image('./images/three.jpg')
|
||||
img_emb4 = encode_image('./images/four.jpg')
|
||||
|
||||
# add records to the *images* collection
|
||||
images.upsert(
|
||||
records=[
|
||||
(
|
||||
"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 Supabase Dashboard by visiting the [Table Editor](https://supabase.com/dashboard/project/_/editor), selecting the `vecs` schema, and the `image_vectors` table.
|
||||
|
||||
## Perform an image search from a text query
|
||||
|
||||
With Supabase Vector we can easily query our embeddings. We can use either an image as the search input or alternatively we can generate an embedding from a string input and use that as the query input:
|
||||
|
||||
```python
|
||||
def search(query_term: Optional[str] = None):
|
||||
if query_term is None:
|
||||
query_term = sys.argv[1]
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
images = vx.get_or_create_collection(name="image_vectors", dimension=1024)
|
||||
|
||||
# Encode text query
|
||||
text_emb = get_embedding_from_titan_multimodal(json.dumps(
|
||||
{
|
||||
"inputText": query_term,
|
||||
"embeddingConfig": {"outputEmbeddingLength": 1024},
|
||||
}
|
||||
))
|
||||
|
||||
# query the collection filtering metadata for "type" = "jpg"
|
||||
results = images.query(
|
||||
data=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 the Amazon Titan multimodal model and Supabase Vector.
|
||||
@@ -6,7 +6,7 @@ subtitle: 'Combine keyword search with semantic search.'
|
||||
sidebar_label: 'Hybrid search'
|
||||
---
|
||||
|
||||
Hybrid search combines [full text search](/docs/guides/database/full-text-search) (searching by keyword) with [semantic search](/docs/guides/ai/concepts#what-are-embeddings) (searching by meaning) to identify results that are both directly and contextually relevant to the user's query.
|
||||
Hybrid search combines [full text search](/docs/guides/ai/keyword-search) (searching by keyword) with [semantic search](/docs/guides/ai/semantic-search) (searching by meaning) to identify results that are both directly and contextually relevant to the user's query.
|
||||
|
||||
## Why would I want to use hybrid search?
|
||||
|
||||
@@ -62,7 +62,7 @@ The table contains 4 columns:
|
||||
- `id` is an auto-generated unique ID for the record. We'll use this later to match records when performing RRF.
|
||||
- `content` contains the actual text we will be searching over.
|
||||
- `fts` is an auto-generated `tsvector` column that is generated using the text in `content`. We will use this for [full text search](/docs/guides/database/full-text-search) (search by keyword).
|
||||
- `embedding` is a [vector column](/docs/guides/ai/vector-columns) that stores the vector generated from our embedding model. We will use this for [semantic search](/docs/guides/ai/concepts#what-are-embeddings) (search by meaning). We chose 512 dimensions for this example, but adjust this to match the size of the embedding vectors generated from your preferred model.
|
||||
- `embedding` is a [vector column](/docs/guides/ai/vector-columns) that stores the vector generated from our embedding model. We will use this for [semantic search](/docs/guides/ai/semantic-search) (search by meaning). We chose 512 dimensions for this example, but adjust this to match the size of the embedding vectors generated from your preferred model.
|
||||
|
||||
Next we'll create indexes on the `fts` and `embedding` columns so that their individual queries will remain fast at scale:
|
||||
|
||||
@@ -95,25 +95,23 @@ as $$
|
||||
with full_text as (
|
||||
select
|
||||
id,
|
||||
row_number() over() as rank_ix
|
||||
-- Note: ts_rank_cd is not indexable but will only rank matches of the where clause
|
||||
-- which shouldn't be too big
|
||||
row_number() over(order by ts_rank_cd(fts, websearch_to_tsquery(query_text)) desc) as rank_ix
|
||||
from
|
||||
documents
|
||||
where
|
||||
fts @@ websearch_to_tsquery(query_text)
|
||||
order by
|
||||
-- Note: this is not indexable but will only rank matches of the where clause
|
||||
-- which shouldn't be too big
|
||||
ts_rank_cd(fts, websearch_to_tsquery(query_text)) desc
|
||||
order by rank_ix
|
||||
limit least(match_count, 30) * 2
|
||||
),
|
||||
semantic as (
|
||||
select
|
||||
id,
|
||||
row_number() over () as rank_ix
|
||||
row_number() over (order by doc_vector <#> query_embedding) as rank_ix
|
||||
from
|
||||
documents
|
||||
order by
|
||||
doc_vector <#> query_embedding
|
||||
order by rank_ix
|
||||
limit least(match_count, 30) * 2
|
||||
)
|
||||
select
|
||||
@@ -227,4 +225,5 @@ For more information on how to create, test, and deploy edge functions, see [Get
|
||||
- [Embedding concepts](/docs/guides/ai/concepts)
|
||||
- [Vector columns](/docs/guides/ai/vector-columns)
|
||||
- [Vector indexes](/docs/guides/ai/vector-indexes)
|
||||
- [Semantic search](/docs/guides/ai/semantic-search)
|
||||
- [Full text (keyword) search](/docs/guides/database/full-text-search)
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
id: 'ai-amazon-bedrock'
|
||||
title: 'Amazon Bedrock'
|
||||
description: 'Learn how to integrate Supabase with Amazon Bedrock, a fully managed service of high-performing foundation models.'
|
||||
sidebar_label: 'Amazon Bedrock'
|
||||
tocVideo: 'A3uND5sgiO0'
|
||||
---
|
||||
|
||||
[Amazon Bedrock](https://aws.amazon.com/bedrock) is a fully managed service that offers a choice of high-performing foundation models (FMs) from leading AI companies like AI21 Labs, Anthropic, Cohere, Meta, Mistral AI, Stability AI, and Amazon. Each model is accessible through a common API which implements a broad set of features to help build generative AI applications with security, privacy, and responsible AI in mind.
|
||||
|
||||
This guide will walk you through an example using Amazon Bedrock SDK with `vecs`. We will create embeddings using the Amazon Titan Embeddings G1 – Text v1.2 (amazon.titan-embed-text-v1) model, insert these embeddings into a PostgreSQL database using vecs, and then query the collection to find the most similar sentences to a given query sentence.
|
||||
|
||||
## Create an Environment
|
||||
|
||||
First, you need to set up your environment. You will need Python 3.7+ with the `vecs` and `boto3` libraries installed.
|
||||
|
||||
You can install the necessary Python libraries using pip:
|
||||
|
||||
```sh
|
||||
pip install vecs boto3
|
||||
```
|
||||
|
||||
You'll also need:
|
||||
|
||||
- [Credentials to your AWS account](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html)
|
||||
- [A Postgres Database with the pgvector extension](hosting.md)
|
||||
|
||||
## Create Embeddings
|
||||
|
||||
Next, we will use Amazon’s Titan Embedding G1 - Text v1.2 model to create embeddings for a set of sentences.
|
||||
|
||||
```python
|
||||
import boto3
|
||||
import vecs
|
||||
import json
|
||||
|
||||
client = boto3.client(
|
||||
'bedrock-runtime',
|
||||
region_name='us-east-1',
|
||||
# Credentials from your AWS account
|
||||
aws_access_key_id='<replace_your_own_credentials>',
|
||||
aws_secret_access_key='<replace_your_own_credentials>',
|
||||
aws_session_token='<replace_your_own_credentials>',
|
||||
)
|
||||
|
||||
dataset = [
|
||||
"The cat sat on the mat.",
|
||||
"The quick brown fox jumps over the lazy dog.",
|
||||
"Friends, Romans, countrymen, lend me your ears",
|
||||
"To be or not to be, that is the question.",
|
||||
]
|
||||
|
||||
embeddings = []
|
||||
|
||||
for sentence in dataset:
|
||||
# invoke the embeddings model for each sentence
|
||||
response = client.invoke_model(
|
||||
body= json.dumps({"inputText": sentence}),
|
||||
modelId= "amazon.titan-embed-text-v1",
|
||||
accept = "application/json",
|
||||
contentType = "application/json"
|
||||
)
|
||||
# collect the embedding from the response
|
||||
response_body = json.loads(response["body"].read())
|
||||
# add the embedding to the embedding list
|
||||
embeddings.append((sentence, response_body.get("embedding"), {}))
|
||||
|
||||
```
|
||||
|
||||
### Store the Embeddings with vecs
|
||||
|
||||
Now that we have our embeddings, we can insert them into a PostgreSQL database using vecs.
|
||||
|
||||
```python
|
||||
import vecs
|
||||
|
||||
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.Client(DB_CONNECTION)
|
||||
|
||||
# create a collection named 'sentences' with 1536 dimensional vectors
|
||||
# to match the default dimension of the Titan Embeddings G1 - Text model
|
||||
sentences = vx.get_or_create_collection(name="sentences", dimension=1536)
|
||||
|
||||
# upsert the embeddings into the 'sentences' collection
|
||||
sentences.upsert(records=embeddings)
|
||||
|
||||
# create an index for the 'sentences' collection
|
||||
sentences.create_index()
|
||||
```
|
||||
|
||||
### Querying for Most Similar Sentences
|
||||
|
||||
Now, we query the `sentences` collection to find the most similar sentences to a sample query sentence. First need to create an embedding for the query sentence. Next, we query the collection we created earlier to find the most similar sentences.
|
||||
|
||||
```python
|
||||
query_sentence = "A quick animal jumps over a lazy one."
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.Client(DB_CONNECTION)
|
||||
|
||||
# create an embedding for the query sentence
|
||||
response = client.invoke_model(
|
||||
body= json.dumps({"inputText": query_sentence}),
|
||||
modelId= "amazon.titan-embed-text-v1",
|
||||
accept = "application/json",
|
||||
contentType = "application/json"
|
||||
)
|
||||
|
||||
response_body = json.loads(response["body"].read())
|
||||
|
||||
query_embedding = response_body.get("embedding")
|
||||
|
||||
# query the 'sentences' collection for the most similar sentences
|
||||
results = sentences.query(
|
||||
data=query_embedding,
|
||||
limit=3,
|
||||
include_value = True
|
||||
)
|
||||
|
||||
# print the results
|
||||
for result in results:
|
||||
print(result)
|
||||
```
|
||||
|
||||
This returns the most similar 3 records and their distance to the query vector.
|
||||
|
||||
```
|
||||
('The quick brown fox jumps over the lazy dog.', 0.27600620558852)
|
||||
('The cat sat on the mat.', 0.609986272479202)
|
||||
('To be or not to be, that is the question.', 0.744849503688346)
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Amazon Bedrock](https://aws.amazon.com/bedrock)
|
||||
- [Amazon Titan](https://aws.amazon.com/bedrock/titan)
|
||||
- [Semantic Image Search with Amazon Titan](/docs/guides/ai/examples/semantic-image-search-amazon-titan)
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
id: 'ai-keyword-search'
|
||||
title: 'Keyword search'
|
||||
description: 'Learn how to search by words or phrases.'
|
||||
subtitle: 'Learn how to search by words or phrases.'
|
||||
sidebar_label: 'Keyword search'
|
||||
---
|
||||
|
||||
Keyword search involves locating documents or records that contain specific words or phrases, primarily based on the exact match between the search terms and the text within the data. It differs from [semantic search](/docs/guides/ai/semantic-search), which interprets the meaning behind the query to provide results that are contextually related, even if the exact words aren't present in the text. Semantic search considers synonyms, intent, and natural language nuances to provide a more nuanced approach to information retrieval.
|
||||
|
||||
In Postgres, keyword search is implemented using [full-text search](/docs/guides/database/full-text-search). It supports indexing and text analysis for data retrieval, focusing on records that match the search criteria. Postgres' full-text search extends beyond simple keyword matching to address linguistic nuances, making it effective for applications that require precise text queries.
|
||||
|
||||
## Why would I want to use keyword search?
|
||||
|
||||
Keyword search is particularly useful in scenarios where precision and specificity matter. It's more effective than semantic search when users are looking for information using exact terminology or specific identifiers. It ensures that results directly contain those terms, reducing the chance of retrieving irrelevant information that might be semantically related but not what the user seeks.
|
||||
|
||||
For example in technical or academic research databases, researchers often search for specific studies, compounds, or concepts identified by certain terms or codes. Searching for a specific chemical compound using its exact molecular formula or a unique identifier will yield more focused and relevant results compared to a semantic search, which could return a wide range of documents discussing the compound in different contexts. Keyword search ensures documents that explicitly mention the exact term are found, allowing users to access the precise data they need efficiently.
|
||||
|
||||
It's also possible to combine keyword search with semantic search to get the best of both worlds. See [Hybrid search](/docs/guides/ai/hybrid-search) for more details.
|
||||
|
||||
## Using full-text search
|
||||
|
||||
For an in-depth guide to Postgres' full-text search, including how to store, index, and query records, see [Full text search](/docs/guides/database/full-text-search).
|
||||
|
||||
## See also
|
||||
|
||||
- [Semantic search](/docs/guides/ai/semantic-search)
|
||||
- [Hybrid search](/docs/guides/ai/hybrid-search)
|
||||
@@ -1,15 +1,11 @@
|
||||
---
|
||||
id: 'ai-generating-embeddings'
|
||||
title: 'Generate Embeddings'
|
||||
subtitle: 'Generate text embeddings using Edge Functions + Transformer.js.'
|
||||
subtitle: 'Generate text embeddings using Edge Functions.'
|
||||
breadcrumb: 'AI Quickstarts'
|
||||
---
|
||||
|
||||
This guide will walk you through how to generate high quality text embeddings in [Edge Functions](/docs/guides/functions) using Transformers.js. Inference is performed directly in Edge Functions using open source Hugging Face models, so no external API is required.
|
||||
|
||||
## What is Transformers.js?
|
||||
|
||||
[Transformers.js](https://huggingface.co/docs/transformers.js/index) is a library that allows you to perform inference using the JavaScript runtime. You can run Hugging Face embedding models through Transformers.js directly in Supabase [Edge Functions](/docs/guides/functions).
|
||||
This guide will walk you through how to generate high quality text embeddings in [Edge Functions](/docs/guides/functions) using its built-in AI inference API, so no external API is required.
|
||||
|
||||
## Build the Edge Function
|
||||
|
||||
@@ -60,89 +56,45 @@ Let's build an Edge Function that will accept an input string and generate an em
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
|
||||
<StepHikeCompact.Details title="Import & configure Transformer.js" fullWidth>
|
||||
<StepHikeCompact.Details title="Setup Inference Session" fullWidth>
|
||||
|
||||
Let's modify the Edge Function to import the Transformers.js client and configure it for the Deno runtime.
|
||||
Let's create a new inference session to be used in the lifetime of this function. Multiple requests can use the same inference session.
|
||||
|
||||
```ts ./supabase/functions/embed/index.ts
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
import { env, pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.5.0'
|
||||
|
||||
// Configuration for Deno runtime
|
||||
env.useBrowserCache = false;
|
||||
env.allowLocalModels = false;
|
||||
```
|
||||
Currently, only the `gte-small` (https://huggingface.co/Supabase/gte-small) text embedding model is supported in Supabase's Edge Runtime.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```ts ./supabase/functions/embed/index.ts
|
||||
const session = new Supabase.ai.Session('gte-small');
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
|
||||
<StepHikeCompact.Details title="Construct embedding pipeline">
|
||||
|
||||
Next let's construct the `pipe()` function we'll use to generate the embeddings. We use the Transformer.js `pipeline()` function to create this.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts ./supabase/functions/embed/index.ts
|
||||
const pipe = await pipeline(
|
||||
'feature-extraction',
|
||||
'Supabase/gte-small',
|
||||
);
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
|
||||
Note the two arguments we pass to `pipeline()`:
|
||||
|
||||
1. The first argument specifies the type of [inference task](https://huggingface.co/docs/transformers.js/index#tasks) to perform. `feature-extraction` is used for embedding generation.
|
||||
2. The second argument specifies which Hugging Face model to use for the embedding generation. Supabase officially supports models available on the [Supabase org](https://huggingface.co/Supabase).
|
||||
|
||||
<Admonition>
|
||||
|
||||
We intentially construct the pipeline before the `serve()` function. This allows us to reuse the pre-loaded model in future warm-start requests, significantly speeding up future queries. Note that you'll only notice this speedup after deploying your function to Supabase - requests in local development will always be a cold-start.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
|
||||
<StepHikeCompact.Details title="Implement request handler">
|
||||
|
||||
Modify our `serve()` request handler to accept an `input` string from the POST request JSON body.
|
||||
Modify our request handler to accept an `input` string from the POST request JSON body.
|
||||
|
||||
Finally let's generate the embedding by:
|
||||
|
||||
1. Calling `pipe()` to perform the embedding generation
|
||||
2. Extracting the embedding from the output as a number array
|
||||
3. Returning it as a JSON response
|
||||
Then generate the embedding by calling `session.run(input)`.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts ./supabase/functions/embed/index.ts
|
||||
serve(async (req) => {
|
||||
Deno.serve(async (req) => {
|
||||
// Extract input string from JSON body
|
||||
const { input } = await req.json();
|
||||
|
||||
// Generate the embedding from the user input
|
||||
const output = await pipe(input, {
|
||||
pooling: 'mean',
|
||||
const embedding = await session.run(input, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
});
|
||||
|
||||
// Extract the embedding output
|
||||
const embedding = Array.from(output.data);
|
||||
|
||||
// Return the embedding
|
||||
return new Response(
|
||||
JSON.stringify({ embedding }),
|
||||
@@ -155,16 +107,16 @@ Let's build an Edge Function that will accept an input string and generate an em
|
||||
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
|
||||
Note the two options we pass to `pipe()`:
|
||||
Note the two options we pass to `session.run()`:
|
||||
|
||||
- `pooling`: The first option sets `pooling` to `mean`. Pooling referes to how token-level embedding representations are compressed into a single sentence embedding that reflects the meaning of the entire sentence. Average pooling is the most common type of pooling for sentence embeddings and is the method supported by Transformers.js.
|
||||
- `normalize`: The second option tells Transformers.js to normalize the embedding vector so that it can be used with distance measures like dot product. A normalized vector means its length (magnitude) is 1 - also referred to as a unit vector. A vector is normalized by dividing each element by the vector's length (magnitude), which maintains its direction but changes its length to 1.
|
||||
- `mean_pool`: The first option sets `pooling` to `mean`. Pooling referes to how token-level embedding representations are compressed into a single sentence embedding that reflects the meaning of the entire sentence. Average pooling is the most common type of pooling for sentence embeddings.
|
||||
- `normalize`: The second option tells to normalize the embedding vector so that it can be used with distance measures like dot product. A normalized vector means its length (magnitude) is 1 - also referred to as a unit vector. A vector is normalized by dividing each element by the vector's length (magnitude), which maintains its direction but changes its length to 1.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={6}>
|
||||
<StepHikeCompact.Step step={5}>
|
||||
|
||||
<StepHikeCompact.Details title="Test it!">
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ on document_sections for select to authenticated using (
|
||||
document_id in (
|
||||
select id
|
||||
from documents
|
||||
where owner_id = auth.uid()
|
||||
where (owner_id = (select auth.uid()))
|
||||
)
|
||||
);
|
||||
```
|
||||
@@ -100,7 +100,7 @@ on document_sections for select to authenticated using (
|
||||
document_id in (
|
||||
select document_id
|
||||
from document_owners
|
||||
where owner_id = auth.uid()
|
||||
where (owner_id = (select auth.uid()))
|
||||
)
|
||||
);
|
||||
```
|
||||
@@ -248,7 +248,7 @@ on document_sections for select to authenticated using (
|
||||
document_id in (
|
||||
select id
|
||||
from documents
|
||||
where owner_id = auth.uid()
|
||||
where (owner_id = (select auth.uid()))
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
id: 'ai-semantic-search'
|
||||
title: 'Semantic search'
|
||||
description: 'Learn how to search by meaning rather than exact keywords.'
|
||||
subtitle: 'Learn how to search by meaning rather than exact keywords.'
|
||||
sidebar_label: 'Semantic search'
|
||||
---
|
||||
|
||||
Semantic search interprets the meaning behind user queries rather than exact [keywords](/docs/guides/ai/keyword-search). It uses machine learning to capture the intent and context behind the query, handling language nuances like synonyms, phrasing variations, and word relationships.
|
||||
|
||||
## When to use semantic search
|
||||
|
||||
Semantic search is useful in applications where the depth of understanding and context is important for delivering relevant results. A good example is in customer support or knowledge base search engines. Users often phrase their problems or questions in various ways, and a traditional keyword-based search might not always retrieve the most helpful documents. With semantic search, the system can understand the meaning behind the queries and match them with relevant solutions or articles, even if the exact wording differs.
|
||||
|
||||
For instance, a user searching for "increase text size on display" might miss articles titled "How to adjust font size in settings" in a keyword-based search system. However, a semantic search engine would understand the intent behind the query and correctly match it to relevant articles, regardless of the specific terminology used.
|
||||
|
||||
It's also possible to combine semantic search with keyword search to get the best of both worlds. See [Hybrid search](/docs/guides/ai/hybrid-search) for more details.
|
||||
|
||||
## How semantic search works
|
||||
|
||||
Semantic search uses an intermediate representation called an “embedding vector” to link database records with search queries. A vector, in the context of semantic search, is a list of numerical values. They represent various features of the text and allow for the semantic comparison between different pieces of text.
|
||||
|
||||
The best way to think of embeddings is by plotting them on a graph, where each embedding is a single point whose coordinates are the numerical values within its vector. Importantly, embeddings are plotted such that similar concepts are positioned close together while dissimilar concepts are far apart. For more details, see [What are embeddings?](/docs/guides/ai/concepts#what-are-embeddings)
|
||||
|
||||
Embeddings are generated using a language model, and embeddings are compared to each other using a similarity metric. The language model is trained to understand the semantics of language, including syntax, context, and the relationships between words. It generates embeddings for both the content in the database and the search queries. Then the similarity metric, often a function like cosine similarity or dot product, is used to compare the query embeddings with the document embeddings (in other words, to measure how close they are to each other on the graph). The documents with embeddings most similar to the query's are deemed the most relevant and are returned as search results.
|
||||
|
||||
## Embedding models
|
||||
|
||||
There are many embedding models available today. Supabase Edge Functions has [built in support](/docs/guides/functions/examples/semantic-search) for the `gte-small` model. Others can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).
|
||||
|
||||
It's crucial to remember that when using embedding models with semantic search, you must use the same model for all embedding comparisons. Comparing embeddings created by different models will yield meaningless results.
|
||||
|
||||
## Semantic search in Postgres
|
||||
|
||||
To implement semantic search in Postgres we use `pgvector` - an extension that allows for efficient storage and retrieval of high-dimensional vectors. These vectors are numerical representations of text (or other types of data) generated by embedding models.
|
||||
|
||||
1. Enable the `pgvector` extension by running:
|
||||
|
||||
```sql
|
||||
create extension vector
|
||||
with
|
||||
schema extensions;
|
||||
```
|
||||
|
||||
1. Create a table to store the embeddings:
|
||||
|
||||
```sql
|
||||
create table documents (
|
||||
id bigint primary key generated always as identity,
|
||||
content text,
|
||||
embedding vector(512)
|
||||
);
|
||||
```
|
||||
|
||||
Or if you have an existing table, you can add a vector column like so:
|
||||
|
||||
```sql
|
||||
alter table documents
|
||||
add column embedding vector(512);
|
||||
```
|
||||
|
||||
In this example, we create a column named `embedding` which uses the newly enabled `vector` data type. The size of the vector (as indicated in parentheses) represents the number of dimensions in the embedding. Here we use 512, but adjust this to match the number of dimensions produced by your embedding model.
|
||||
|
||||
For more details on vector columns, including how to generate embeddings and store them, see [Vector columns](/docs/guides/ai/vector-columns).
|
||||
|
||||
### Similarity metric
|
||||
|
||||
`pgvector` support 3 operators for computing distance between embeddings:
|
||||
|
||||
| **Operator** | **Description** |
|
||||
| ------------ | ---------------------- |
|
||||
| `<->` | Euclidean distance |
|
||||
| `<#>` | negative inner product |
|
||||
| `<=>` | cosine distance |
|
||||
|
||||
These operators are used directly in your SQL query to retrieve records that are most similar to the user's search query. Choosing the right operator depends on your needs. Inner product (also known as dot product) tends to be the fastest if your vectors are normalized.
|
||||
|
||||
The easiest way to perform semantic search in Postgres in by creating a function:
|
||||
|
||||
```sql
|
||||
-- Match documents using cosine distance (<=>)
|
||||
create or replace function match_documents (
|
||||
query_embedding vector(512),
|
||||
match_threshold float,
|
||||
match_count int
|
||||
)
|
||||
returns setof documents
|
||||
language sql
|
||||
as $$
|
||||
select *
|
||||
from documents
|
||||
where documents.embedding <=> query_embedding < 1 - match_threshold
|
||||
order by documents.embedding <=> query_embedding asc
|
||||
limit least(match_count, 200);
|
||||
$$;
|
||||
```
|
||||
|
||||
Here we create a function `match_documents` that accepts three parameters:
|
||||
|
||||
1. `query_embedding`: a one-time embedding generated for the user's search query. Here we set the size to 512, but adjust this to match the number of dimensions produced by your embedding model.
|
||||
1. `match_threshold`: the minimum similarity between embeddings. This is a value between 1 and -1, where 1 is most similar and -1 is most dissimilar.
|
||||
1. `match_count`: the maximum number of results to return. Note the query may return less than this number if `match_threshold` resulted in a small shortlist. Limited to 200 records to avoid unintentionally overloading your database.
|
||||
|
||||
In this example, we return a `setof documents` and refer to `documents` throughout the query. Adjust this to use the relevant tables in your application.
|
||||
|
||||
You'll notice we are using the cosine distance (`<=>`) operator in our query. Cosine distance is a safe default when you don't know whether or not your embeddings are normalized. If you know for a fact that they are normalized (for example, your embedding is returned from OpenAI), you can use negative inner product (`<#>`) for better performance:
|
||||
|
||||
```sql
|
||||
-- Match documents using negative inner product (<#>)
|
||||
create or replace function match_documents (
|
||||
query_embedding vector(512),
|
||||
match_threshold float,
|
||||
match_count int
|
||||
)
|
||||
returns setof documents
|
||||
language sql
|
||||
as $$
|
||||
select *
|
||||
from documents
|
||||
where documents.embedding <#> query_embedding < -match_threshold
|
||||
order by documents.embedding <#> query_embedding asc
|
||||
limit least(match_count, 200);
|
||||
$$;
|
||||
```
|
||||
|
||||
Note that since `<#>` is negative, we negate `match_threshold` accordingly in the `where` clause. For more information on the different operators, see the [pgvector docs](https://github.com/pgvector/pgvector?tab=readme-ov-file#vector-operators).
|
||||
|
||||
### Calling from your application
|
||||
|
||||
Finally you can execute this function from your application. If you are using a Supabase client library such as [`supabase-js`](https://github.com/supabase/supabase-js), you can invoke it using the `rpc()` method:
|
||||
|
||||
```tsx
|
||||
const { data: documents } = await supabase.rpc('match_documents', {
|
||||
query_embedding: embedding, // pass the query embedding
|
||||
match_threshold: 0.78, // choose an appropriate threshold for your data
|
||||
match_count: 10, // choose the number of matches
|
||||
})
|
||||
```
|
||||
|
||||
You can also call this method directly from SQL:
|
||||
|
||||
```sql
|
||||
select *
|
||||
from match_documents(
|
||||
'[...]'::vector(512), -- pass the query embedding
|
||||
0.78, -- chose an appropriate threshold for your data
|
||||
10 -- choose the number of matches
|
||||
);
|
||||
```
|
||||
|
||||
In this scenario, you'll likely use a Postgres client library to establish a direct connection from your application to the database. It's best practice to parameterize your arguments before executing the query.
|
||||
|
||||
## Next steps
|
||||
|
||||
As your database scales, you will need an index on your vector columns to maintain fast query speeds. See [Vector indexes](/docs/guides/ai/vector-indexes) for an in-depth guide on the different types of indexes and how they work.
|
||||
|
||||
## See also
|
||||
|
||||
- [Embedding concepts](/docs/guides/ai/concepts)
|
||||
- [Vector columns](/docs/guides/ai/vector-columns)
|
||||
- [Vector indexes](/docs/guides/ai/vector-indexes)
|
||||
- [Hybrid search](/docs/guides/ai/hybrid-search)
|
||||
- [Keyword search](/docs/guides/ai/keyword-search)
|
||||
@@ -109,7 +109,7 @@ Similarity search is the most common use case for vectors. `pgvector` support 3
|
||||
|
||||
Choosing the right operator depends on your needs. Dot product tends to be the fastest if your vectors are normalized. 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:
|
||||
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 (
|
||||
|
||||
@@ -169,5 +169,12 @@ final data = await supabase.from('todos').select('*');
|
||||
response = supabase.table('todos').select("*").execute()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
```swift
|
||||
let response = try await supabase.from("todos").select()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
@@ -13,6 +13,7 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| 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) |
|
||||
| Swift | [supabase-swift](https://github.com/supabase/supabase-swift) | [Docs](https://supabase.com/docs/reference/swift/introduction) |
|
||||
|
||||
## Community libraries
|
||||
|
||||
@@ -23,5 +24,4 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
|
||||
| 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) | [Docs](https://supabase.com/docs/reference/swift/introduction) |
|
||||
| Godot Engine (GDScript) | [supabase-gdscript](https://github.com/supabase-community/godot-engine.supabase) | |
|
||||
@@ -125,6 +125,12 @@ export type Database = MergeDeep<
|
||||
>
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
To use `MergeDeep`, set `compilerOptions.strictNullChecks` to `true` in your `tsconfig.json`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
You can also override the type of an individual successful response if needed:
|
||||
|
||||
```ts
|
||||
|
||||
@@ -29,10 +29,11 @@ Here's a quick, 2 minute tour of the Auth features built-in to Supabase:
|
||||
|
||||
You can authenticate your users in several ways:
|
||||
|
||||
- Password-based methods such as email or phone and password
|
||||
- Password-based methods such as [email](/docs/guides/auth/auth-email) or [phone](/docs/guides/auth/phone-login) and password
|
||||
- Passwordless methods such as sending a magiclink or one-time password (OTP)
|
||||
- OAuth social providers
|
||||
- SAML SSO
|
||||
- [Anonymous Sign-ins](/docs/guides/auth/auth-anonymous)
|
||||
|
||||
### Providers
|
||||
|
||||
@@ -109,7 +110,7 @@ const { data, error } = await supabase
|
||||
// => { id: 'd0714948', name: 'Jane' }
|
||||
```
|
||||
|
||||
... you can simply define a rule on your database table, `auth.uid() = user_id`, and your request will return the rows which pass the rule, even when you remove the filter from your middleware:
|
||||
... you can simply define a rule on your database table, `(select auth.uid()) = user_id`, and your request will return the rows which pass the rule, even when you remove the filter from your middleware:
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.from('users').select('user_id, name')
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
---
|
||||
id: 'auth-anonymous'
|
||||
title: 'Anonymous Sign-Ins'
|
||||
subtitle: 'Create and use anonymous users to authenticate with Supabase'
|
||||
---
|
||||
|
||||
[Enable Anonymous Sign-Ins](/dashboard/project/_/settings/auth) to build apps which provide users an authenticated experience without requiring users to enter an email address, password, use an OAuth provider or provide any other PII (Personally Identifiable Information). Later, when ready, the user can link an authentication method to their account.
|
||||
|
||||
<Admonition type="note" label="Anonymous user vs the anon key">
|
||||
|
||||
Calling `signInAnonymously()` creates an anonymous user. It's just like a permanent user, except the user can't access their account if they sign out, clear browsing data, or use another device.
|
||||
|
||||
Like permanent users, the `authenticated` Postgres role will be used when using the Data APIs to access your project. JWTs for these users will have an `is_anonymous` claim which you can use to distinguish in RLS policies.
|
||||
|
||||
This is different from the `anon` API key which does not create a user and can be used to implement public access to your database as it uses the `anonymous` Postgres role.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Anonymous sign-ins can be used to build:
|
||||
|
||||
- E-commerce applications, such as shopping carts before check-out
|
||||
- Full-feature demos without collecting personal information
|
||||
- Temporary or throw-away accounts
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Review your existing RLS policies before enabling anonymous sign-ins. Anonymous users use the `authenticated` role. To distinguish between anonymous users and permanent users, your policies need to check the `is_anonymous` field of the user's JWT.
|
||||
|
||||
See the [Access control section](#access-control) for more details.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="note" label="Self hosting and local development">
|
||||
|
||||
For self-hosting, you can update your project configuration using the files and environment variables provided. See the [local development docs](/docs/guides/cli/config) for more details.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Sign in anonymously
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
queryGroup="language"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Call the [`signInAnonymously()`](/docs/reference/javascript/auth-signinanonymously) method:
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.signInAnonymously()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Flutter">
|
||||
|
||||
Call the [`signInAnonymously()`](/docs/reference/dart/auth-signinanonymously) method:
|
||||
|
||||
```dart
|
||||
await supabase.auth.signInAnonymously();
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
Call the [`signInAnonymously()`](/docs/reference/swift/auth-signinanonymously) method:
|
||||
|
||||
```swift
|
||||
let session = try await supabase.auth.signInAnonymously()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
Call the [`signInAnonymously()`](/docs/reference/kotlin/auth-signinanonymously) method:
|
||||
|
||||
```kotlin
|
||||
supabase.auth.signInAnonymously()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Convert an anonymous user to a permanent user
|
||||
|
||||
Converting an anonymous user to a permanent user requires [linking an identity](/docs/guides/auth/auth-identity-linking#manual-linking-beta) to the user. This requires you to [enable manual linking](/dashboard/project/_/settings/auth) in your Supabase project.
|
||||
|
||||
### Link an Email / Phone identity
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
queryGroup="language"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
You can use the [`updateUser()`](/docs/reference/javascript/auth-updateuser) method to link an email or phone identity to the anonymous user. To add a password for the anonymous user, the user's email or phone number needs to be verified first.
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.updateUser({ email: 'example@email.com' })
|
||||
|
||||
// verify the user's email by clicking on the email change link
|
||||
// or entering the 6-digit OTP sent to the email address
|
||||
|
||||
// once the user has been verified, update the password
|
||||
const { data, error } = await supabase.auth.updateUser({ password: 'password' })
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Flutter">
|
||||
|
||||
You can use the [`updateUser()`](/docs/reference/dart/auth-updateuser) method to link an email or phone identity to the anonymous user.
|
||||
|
||||
```dart
|
||||
await supabase.auth.updateUser(UserAttributes(email: 'example@email.com'));
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
You can use the [`updateUser()`](/docs/reference/swift/auth-updateuser) method to link an email or phone identity to the anonymous user.
|
||||
|
||||
```swift
|
||||
try await supabase.auth.updateUser(
|
||||
user: UserAttributes(email: "example@email.com")
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
You can use the [`updateUser()`](/docs/reference/kotlin/auth-updateuser) method to link an email or phone identity to the anonymous user.
|
||||
|
||||
```kotlin
|
||||
supabase.auth.updateUser {
|
||||
email = "example@email.com"
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Link an OAuth identity
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
queryGroup="language"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
You can use the [`linkIdentity()`](/docs/reference/javascript/auth-linkidentity) method to link an oauth identity to the anonymous user.
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.linkIdentity({ provider: 'google' })
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Flutter">
|
||||
|
||||
You can use the [`linkIdentity()`](/docs/reference/dart/auth-linkidentity) method to link an OAuth identity to the anonymous user.
|
||||
|
||||
```dart
|
||||
await supabase.auth.linkIdentity(OAuthProvider.google);
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
You can use the [`linkIdentity()`](/docs/reference/swift/auth-linkidentity) method to link an OAuth identity to the anonymous user.
|
||||
|
||||
```swift
|
||||
try await supabase.auth.linkIdentity(provider: .google)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
You can use the [`linkIdentity()`](/docs/reference/kotlin/auth-linkidentity) method to link an oauth identity to the anonymous user.
|
||||
|
||||
```kotlin
|
||||
supabase.auth.linkIdentity(Google)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Access control
|
||||
|
||||
An anonymous user assumes the `authenticated` role just like a permanent user. You can use row-level security (RLS) policies to differentiate between an anonymous user and a permanent user by checking for the `is_anonymous` claim in the JWT returned by `auth.jwt()`:
|
||||
|
||||
```sql
|
||||
create policy "Only permanent users can post to the news feed"
|
||||
on news_feed as restrictive for insert
|
||||
to authenticated
|
||||
with check ((select (auth.jwt()->>'is_anonymous')::boolean) is false );
|
||||
|
||||
create policy "Anonymous and permanent users can view the news feed"
|
||||
on news_feed for select
|
||||
to authenticated
|
||||
using ( true );
|
||||
```
|
||||
|
||||
<Admonition variant="note" label="Use restrictive policies">
|
||||
|
||||
RLS policies are permissive by default, which means that they are combined using an "OR" operator when multiple policies are applied. It is important to construct restrictive policies to ensure that the checks for an anonymous user are always enforced when combined with other policies.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Resolving identity conflicts
|
||||
|
||||
Depending on your application requirements, data conflicts can arise when an anonymous user is converted to a permanent user. For example, in the context of an e-commerce application, an anonymous user would be allowed to add items to the shopping cart without signing up / signing in. When they decide to sign-in to an existing account, you will need to decide how you want to resolve data conflicts in the shopping cart:
|
||||
|
||||
1. Overwrite the items in the cart with those in the existing account
|
||||
2. Overwrite the items in the cart with those from the anonymous user
|
||||
3. Merge the items in the cart together
|
||||
|
||||
## Abuse prevention and rate limits
|
||||
|
||||
Since anonymous users are stored in your database, bad actors can abuse the endpoint to increase your database size drastically. It is strongly recommended to [enable invisible Captcha or Cloudflare Turnstile](/docs/guides/auth/auth-captcha) to prevent abuse for anonymous sign-ins. An IP-based rate limit is enforced at 30 requests per hour which can be modified in your [dashboard](/dashboard/project/_/auth/rate-limits). You can refer to the full list of rate limits [here](/docs/guides/platform/going-into-prod#rate-limiting-resource-allocation--abuse-prevention).
|
||||
|
||||
## Automatic cleanup
|
||||
|
||||
Automatic cleanup of anonymous users is currently not available. Instead, you can delete anonymous users from your project by running the following SQL:
|
||||
|
||||
```sql
|
||||
-- deletes anonymous users created more than 30 days ago
|
||||
delete from auth.users
|
||||
where is_anonymous is true and created_at < now() - interval '30 days';
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
- [Supabase Kotlin Client](https://github.com/supabase-community/supabase-kt)
|
||||
@@ -122,7 +122,7 @@ Now we'll write our policy, again in SQL, but note it's also possible to add via
|
||||
```sql
|
||||
CREATE POLICY user_update_own_scores ON my_scores
|
||||
FOR ALL
|
||||
USING (auth.uid() = user_id);
|
||||
USING ((select auth.uid()) = user_id);
|
||||
```
|
||||
|
||||
Now, assuming you have an active session in your javascript/supabase-js environment you can do:
|
||||
@@ -149,7 +149,7 @@ Once you get the hang of policies you can start to get a little bit fancy. Let's
|
||||
create policy "Only Blizzard staff can update leaderboard"
|
||||
on my_scores
|
||||
for update using (
|
||||
right(auth.jwt() ->> 'email', 13) = '@blizzard.com'
|
||||
right((select auth.jwt() ->> 'email'), 13) = '@blizzard.com'
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
@@ -57,6 +57,19 @@ Future<void> signUpNewUser() async {
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
To sign up the user, call [signUp()](/docs/reference/swift/auth-signup) with their email address and password:
|
||||
|
||||
```swift
|
||||
let response = try await supabase.auth.signUp(
|
||||
email: "example@email.com",
|
||||
password: "example-password",
|
||||
redirectTo: URL(string: "https://example.com/welcome")
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -110,6 +123,18 @@ Future<void> signInWithEmail() async {
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
When your user signs in, call [signIn(email:password:)](/docs/reference/swift/auth-signinwithpassword) with their email address and password:
|
||||
|
||||
```swift
|
||||
try await supabase.auth.signIn(
|
||||
email: "example@email.com",
|
||||
password: "example-password"
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -157,6 +182,15 @@ Future<void> signOut() async {
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
When your user signs out, call [signOut()](/docs/reference/swift/auth-signout) to remove the session from the local storage:
|
||||
|
||||
```swift
|
||||
try await supabase.auth.signOut()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -173,7 +207,7 @@ suspend fun logout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
- [Supabase Kotlin Client](https://github.com/supabase-community/supabase-kt)
|
||||
@@ -11,9 +11,27 @@ The `auth-helpers` package has been replaced with the `@supabase/ssr` package. W
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-foreground-light border-b mt-8 pb-2"
|
||||
>
|
||||
|
||||
<Accordion.Item
|
||||
header="See legacy docs"
|
||||
id="legacy-docs"
|
||||
>
|
||||
|
||||
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).
|
||||
<Admonition type="note">
|
||||
|
||||
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).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Install the Next.js helper library
|
||||
|
||||
@@ -914,3 +932,7 @@ import { Database } from '../database.types'
|
||||
|
||||
const supabaseClient = useSupabaseClient<Database>()
|
||||
```
|
||||
|
||||
</Accordion.Item>
|
||||
|
||||
</Accordion>
|
||||
@@ -11,6 +11,20 @@ The `auth-helpers` package has been replaced with the `@supabase/ssr` package. W
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-foreground-light border-b mt-8 pb-2"
|
||||
>
|
||||
|
||||
<Accordion.Item
|
||||
header="See legacy docs"
|
||||
id="legacy-docs"
|
||||
>
|
||||
|
||||
The [Next.js Auth Helpers package](https://github.com/supabase/auth-helpers) configures Supabase Auth to store the user's `session` in a `cookie`, rather than `localStorage`. This makes it available across the client and server of the App Router - [Client Components](/docs/guides/auth/auth-helpers/nextjs#client-components), [Server Components](/docs/guides/auth/auth-helpers/nextjs#server-components), [Server Actions](/docs/guides/auth/auth-helpers/nextjs#server-actions), [Route Handlers](/docs/guides/auth/auth-helpers/nextjs#route-handlers) and [Middleware](/docs/guides/auth/auth-helpers/nextjs#middleware). The `session` is automatically sent along with any requests to Supabase.
|
||||
|
||||
<div className="video-container">
|
||||
@@ -1312,3 +1326,7 @@ export default function() {
|
||||
```
|
||||
|
||||
For an example of creating multiple Supabase clients, check [Singleton section](/docs/guides/auth/auth-helpers/nextjs#singleton) above.
|
||||
|
||||
</Accordion.Item>
|
||||
|
||||
</Accordion>
|
||||
@@ -11,6 +11,20 @@ We generally recommend using the new `@supabase/ssr` package instead of `auth-he
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-foreground-light border-b mt-8 pb-2"
|
||||
>
|
||||
|
||||
<Accordion.Item
|
||||
header="See legacy docs"
|
||||
id="legacy-docs"
|
||||
>
|
||||
|
||||
This submodule provides convenience helpers for implementing user authentication in Remix applications.
|
||||
|
||||
<div className="video-container">
|
||||
@@ -22,7 +36,11 @@ This submodule provides convenience helpers for implementing user authentication
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
> For a complete implementation example, check out [this free egghead course](https://egghead.io/courses/build-a-realtime-chat-app-with-remix-and-supabase-d36e2618) or [this GitHub repo](https://github.com/supabase/auth-helpers/tree/main/examples/remix).
|
||||
<Admonition type="tip">
|
||||
|
||||
For a complete implementation example, check out [this free egghead course](https://egghead.io/courses/build-a-realtime-chat-app-with-remix-and-supabase-d36e2618) or [this GitHub repo](https://github.com/supabase/auth-helpers/tree/main/examples/remix).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Install the Remix helper library
|
||||
|
||||
@@ -165,7 +183,11 @@ export const loader = async ({ request }) => {
|
||||
}
|
||||
```
|
||||
|
||||
> Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Loader` function.
|
||||
<Admonition type="tip">
|
||||
|
||||
Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Loader` function.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
@@ -195,7 +217,11 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
}
|
||||
```
|
||||
|
||||
> Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Loader` function.
|
||||
<Admonition type="tip">
|
||||
|
||||
Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Loader` function.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
@@ -237,7 +263,11 @@ export const action = async ({ request }) => {
|
||||
}
|
||||
```
|
||||
|
||||
> Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Action` function.
|
||||
<Admonition type="tip">
|
||||
|
||||
Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Action` function.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
@@ -268,7 +298,11 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
}
|
||||
```
|
||||
|
||||
> Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Action` function.
|
||||
<Admonition type="tip">
|
||||
|
||||
Supabase will set cookie headers to manage the user's auth session, therefore, the `response.headers` must be returned from the `Action` function.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
@@ -327,7 +361,11 @@ export const loader = () => {
|
||||
}
|
||||
```
|
||||
|
||||
> These may not be stored in `process.env` for environments other than Node.
|
||||
<Admonition type="tip">
|
||||
|
||||
These may not be stored in `process.env` for environments other than Node.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Next, we call the `useLoaderData` hook in our component to get the `env` object.
|
||||
|
||||
@@ -361,7 +399,11 @@ export const loader = ({}: LoaderFunctionArgs) => {
|
||||
}
|
||||
```
|
||||
|
||||
> These may not be stored in `process.env` for environments other than Node.
|
||||
<Admonition type="tip">
|
||||
|
||||
These may not be stored in `process.env` for environments other than Node.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Next, we call the `useLoaderData` hook in our component to get the `env` object.
|
||||
|
||||
@@ -544,7 +586,11 @@ useEffect(() => {
|
||||
|
||||
</Tabs>
|
||||
|
||||
> Check out [this repo](https://github.com/supabase/auth-helpers/tree/main/examples/remix) for full implementation example
|
||||
<Admonition type="tip">
|
||||
|
||||
Check out [this repo](https://github.com/supabase/auth-helpers/tree/main/examples/remix) for full implementation example
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -757,7 +803,11 @@ export default function Index() {
|
||||
|
||||
</Tabs>
|
||||
|
||||
> Ensure you have [enabled replication](https://supabase.com/dashboard/project/_/database/replication) on the table you are subscribing to.
|
||||
<Admonition type="tip">
|
||||
|
||||
Ensure you have [enabled replication](https://supabase.com/dashboard/project/_/database/publications) on the table you are subscribing to.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Migration guide
|
||||
|
||||
@@ -782,3 +832,7 @@ supabaseClient.auth.signUp({
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</Accordion.Item>
|
||||
|
||||
</Accordion>
|
||||
@@ -11,6 +11,20 @@ We generally recommend using the new `@supabase/ssr` package instead of `auth-he
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-foreground-light border-b mt-8 pb-2"
|
||||
>
|
||||
|
||||
<Accordion.Item
|
||||
header="See legacy docs"
|
||||
id="legacy-docs"
|
||||
>
|
||||
|
||||
This submodule provides convenience helpers for implementing user authentication in [SvelteKit](https://kit.svelte.dev/) applications.
|
||||
|
||||
## Configuration
|
||||
@@ -46,8 +60,6 @@ PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
|
||||
Create a new `hooks.server.js` file in the root of your project and populate with the following to retrieve the user session.
|
||||
|
||||
The session information from `getSession` is supplied to the Supabase client so it can retrieve the auth token. This is safe, since the auth token signature will be revalidated on the Auth server. But you shouldn't trust the unsigned data that is stored alongside the auth token:
|
||||
|
||||
<GetSessionWarning />
|
||||
|
||||
```js src/hooks.server.js
|
||||
@@ -63,15 +75,23 @@ export const handle = async ({ event, resolve }) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser()
|
||||
if (error) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -102,15 +122,23 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser()
|
||||
if (error) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -189,17 +217,18 @@ In order to get the most out of TypeScript and it's intellisense, you should imp
|
||||
```ts src/app.d.ts
|
||||
// src/app.d.ts
|
||||
|
||||
import { SupabaseClient, Session } from '@supabase/supabase-js'
|
||||
import { SupabaseClient, Session, User } from '@supabase/supabase-js'
|
||||
import { Database } from './DatabaseDefinitions'
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
supabase: SupabaseClient<Database>
|
||||
getSession(): Promise<Session | null>
|
||||
safeGetSession(): Promise<{ session: Session | null; user: User | null }>
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null
|
||||
user: User | null
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
@@ -234,9 +263,12 @@ To make the session available across the UI, including pages and layouts, it is
|
||||
|
||||
```js src/routes/+layout.server.js
|
||||
// src/routes/+layout.server.js
|
||||
export const load = async ({ locals: { getSession } }) => {
|
||||
export const load = async ({ locals: { safeGetSession } }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
|
||||
return {
|
||||
session: await getSession(),
|
||||
session,
|
||||
user,
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -246,9 +278,12 @@ export const load = async ({ locals: { getSession } }) => {
|
||||
|
||||
```ts src/routes/+layout.server.ts
|
||||
// src/routes/+layout.server.ts
|
||||
export const load = async ({ locals: { getSession } }) => {
|
||||
export const load = async ({ locals: { safeGetSession } }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
|
||||
return {
|
||||
session: await getSession(),
|
||||
session,
|
||||
user,
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -284,6 +319,11 @@ export const load = async ({ fetch, data, depends }) => {
|
||||
serverSession: data.session,
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
@@ -312,6 +352,11 @@ export const load = async ({ fetch, data, depends }) => {
|
||||
serverSession: data.session,
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
@@ -532,8 +577,8 @@ Wrap an API Route to check that the user has a valid session. If they're not log
|
||||
// src/routes/api/protected-route/+server.ts
|
||||
import { json, error } from '@sveltejs/kit'
|
||||
|
||||
export const GET = async ({ locals: { supabase, getSession } }) => {
|
||||
const session = await getSession()
|
||||
export const GET = async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
throw error(401, { message: 'Unauthorized' })
|
||||
@@ -555,8 +600,8 @@ Wrap an Action to check that the user has a valid session. If they're not logged
|
||||
import { error, fail } from '@sveltejs/kit'
|
||||
|
||||
export const actions = {
|
||||
createPost: async ({ request, locals: { supabase, getSession } }) => {
|
||||
const session = await getSession()
|
||||
createPost: async ({ request, locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
@@ -615,15 +660,21 @@ async function supabase({ event, resolve }) {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) return { session: null, user: null }
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -636,7 +687,7 @@ async function supabase({ event, resolve }) {
|
||||
async function authorization({ event, resolve }) {
|
||||
// protect requests to all routes that start with /protected-routes
|
||||
if (event.url.pathname.startsWith('/protected-routes') && event.request.method === 'GET') {
|
||||
const session = await event.locals.getSession()
|
||||
const { session } = await event.locals.safeGetSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
redirect(303, '/')
|
||||
@@ -645,7 +696,7 @@ async function authorization({ event, resolve }) {
|
||||
|
||||
// protect POST requests to all routes that start with /protected-posts
|
||||
if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') {
|
||||
const session = await event.locals.getSession()
|
||||
const { session } = await event.locals.safeGetSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
throw error(303, '/')
|
||||
@@ -677,15 +728,21 @@ async function supabase({ event, resolve }) {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) return { session: null, user: null }
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -698,7 +755,7 @@ async function supabase({ event, resolve }) {
|
||||
async function authorization({ event, resolve }) {
|
||||
// protect requests to all routes that start with /protected-routes
|
||||
if (event.url.pathname.startsWith('/protected-routes') && event.request.method === 'GET') {
|
||||
const session = await event.locals.getSession()
|
||||
const { session } = await event.locals.safeGetSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
redirect(303, '/')
|
||||
@@ -707,7 +764,7 @@ async function authorization({ event, resolve }) {
|
||||
|
||||
// protect POST requests to all routes that start with /protected-posts
|
||||
if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') {
|
||||
const session = await event.locals.getSession()
|
||||
const { session } = await event.locals.safeGetSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
throw error(303, '/')
|
||||
@@ -896,15 +953,21 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) return { session: null, user: null }
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -1055,17 +1118,18 @@ declare namespace App {
|
||||
|
||||
```ts src/app.d.ts
|
||||
// src/app.d.ts
|
||||
import { SupabaseClient, Session } from '@supabase/supabase-js'
|
||||
import { SupabaseClient, Session, User } from '@supabase/supabase-js'
|
||||
import { Database } from './DatabaseDefinitions'
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
supabase: SupabaseClient<Database>
|
||||
getSession(): Promise<Session | null>
|
||||
safeGetSession(): Promise<{ session: Session | null; user: User | null }>
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null
|
||||
user: User | null
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
@@ -1196,7 +1260,7 @@ import type { RequestHandler } from './$types'
|
||||
import { json, error } from '@sveltejs/kit'
|
||||
|
||||
export const GET: RequestHandler = async ({ locals: { supabase, getSession } }) => {
|
||||
const session = await getSession()
|
||||
const { session } = await getSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
throw error(401, { message: 'Unauthorized' })
|
||||
@@ -1966,3 +2030,7 @@ export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient
|
||||
|
||||
- [Auth Helpers Source code](https://github.com/supabase/auth-helpers)
|
||||
- [SvelteKit example](https://github.com/supabase/auth-helpers/tree/main/examples/sveltekit)
|
||||
|
||||
</Accordion.Item>
|
||||
|
||||
</Accordion>
|
||||
@@ -54,6 +54,15 @@ Supabase Auth allows a user to initiate identity linking with a different email
|
||||
const { data, error } = await supabase.auth.linkIdentity({ provider: 'google' })
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
Supabase Auth allows a user to initiate identity linking with a different email address when they are logged in. To link an OAuth identity to the user, call [`linkIdentity()`](/docs/reference/swift/auth-linkidentity):
|
||||
|
||||
```swift
|
||||
try await supabase.auth.linkIdentity(provider: .google)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -94,6 +103,22 @@ const googleIdentity = identities.find((identity) => identity.provider === 'goog
|
||||
const { data, error } = await supabase.auth.unlinkIdentity(googleIdentity)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
You can use [`getUserIdentities()`](/docs/reference/swift/auth-getuseridentities) to fetch all the identities linked to a user. Then, call [`unlinkIdentity()`](/docs/reference/swift/auth-unlinkidentity) to unlink the identity. The user needs to be logged in and have at least 2 linked identities in order to unlink an existing identity.
|
||||
|
||||
```swift
|
||||
// retrieve all identities linked to a user
|
||||
let identities = try await supabase.auth.userIdentities()
|
||||
|
||||
// find the google identity linked to the user
|
||||
let googleIdentity = identities.first { $0.provider == .google }
|
||||
|
||||
// unlink the google identity from the user
|
||||
try await supabase.auth.unlinkIdentity(googleIdentity)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
|
||||
@@ -457,7 +457,7 @@ create policy "Policy name."
|
||||
on table_name
|
||||
as restrictive
|
||||
to authenticated
|
||||
using (auth.jwt()->>'aal' = 'aal2');
|
||||
using ((select auth.jwt()->>'aal') = 'aal2');
|
||||
```
|
||||
|
||||
- Here the policy will not accept any JWTs with an `aal` claim other than
|
||||
@@ -475,14 +475,14 @@ create policy "Policy name."
|
||||
as restrictive -- very important!
|
||||
to authenticated
|
||||
using
|
||||
(array[auth.jwt()->>'aal'] <@ (
|
||||
(array[(select auth.jwt()->>'aal')] <@ (
|
||||
select
|
||||
case
|
||||
when created_at >= '2022-12-12T00:00:00Z' then array['aal2']
|
||||
else array['aal1', 'aal2']
|
||||
end as aal
|
||||
from auth.users
|
||||
where auth.uid() = id));
|
||||
where (select auth.uid()) = id));
|
||||
```
|
||||
|
||||
- The policy will accept both `aal1` and `aal2` for users with a `created_at`
|
||||
@@ -504,14 +504,14 @@ create policy "Policy name."
|
||||
as restrictive -- very important!
|
||||
to authenticated
|
||||
using (
|
||||
array[auth.jwt()->>'aal'] <@ (
|
||||
array[(select auth.jwt()->>'aal')] <@ (
|
||||
select
|
||||
case
|
||||
when count(id) > 0 then array['aal2']
|
||||
else array['aal1', 'aal2']
|
||||
end as aal
|
||||
from auth.mfa_factors
|
||||
where auth.uid() = user_id and status = 'verified'
|
||||
where ((select auth.uid()) = user_id) and status = 'verified'
|
||||
));
|
||||
```
|
||||
|
||||
@@ -600,7 +600,7 @@ Use the `supabase.auth.getAuthenticatorAssuranceLevel()` method to get easy acce
|
||||
You can use this PostgreSQL snippet in RLS policies, too:
|
||||
|
||||
```sql
|
||||
jsonb_path_query(auth.jwt(), '$.amr[0]')
|
||||
jsonb_path_query((select auth.jwt()), '$.amr[0]')
|
||||
```
|
||||
|
||||
- [`jsonb_path_query(json, path)`](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSON-PROCESSING-TABLE)
|
||||
|
||||
@@ -72,7 +72,7 @@ Supabase Auth provides these [configuration options](/dashboard/project/_/settin
|
||||
create policy "policy_name"
|
||||
ON public.posts
|
||||
for insert to authenticated with check (
|
||||
auth.jwt()->>'email_verified' is true
|
||||
(select auth.jwt()->>'email_verified') is true
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ subtitle: 'Set up redirect urls with Supabase Auth.'
|
||||
|
||||
## Overview
|
||||
|
||||
When using [passwordless sign-ins](/docs/reference/javascript/auth-signinwithotp) or [third-party providers](/docs/reference/javascript/auth-signinwithoauth#sign-in-using-a-third-party-provider-with-redirect), the Supabase client library methods provide a `redirectTo` parameter to specify where to redirect the user to after authentication. By default, the user will be redirected to the [`SITE_URL`](/docs/reference/auth/config#site_url) but you can modify the `SITE_URL` or add additional redirect URLs to the [allow list](https://supabase.com/dashboard/project/_/auth/url-configuration). Once you've added necessary URLs to the allow list, you can specify the URL you want the user to be redirected to in the `redirectTo` parameter.
|
||||
When using [passwordless sign-ins](/docs/reference/javascript/auth-signinwithotp) or [third-party providers](/docs/reference/javascript/auth-signinwithoauth#sign-in-using-a-third-party-provider-with-redirect), the Supabase client library methods provide a `redirectTo` parameter to specify where to redirect the user to after authentication. By default, the user will be redirected to the [`SITE_URL`](/docs/guides/auth/concepts/redirect-urls) but you can modify the `SITE_URL` or add additional redirect URLs to the [allow list](https://supabase.com/dashboard/project/_/auth/url-configuration). Once you've added necessary URLs to the allow list, you can specify the URL you want the user to be redirected to in the `redirectTo` parameter.
|
||||
|
||||
## Use wildcards in redirect URLs
|
||||
|
||||
@@ -76,8 +76,37 @@ const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
})
|
||||
```
|
||||
|
||||
## Email templates when using `redirectTo`
|
||||
|
||||
When using a `redirectTo` option, you may need to replace the `{{ .SiteURL }}` with `{{ .RedirectTo }}` in your email templates. See the [Email Templates guide](/docs/guides/auth/auth-email-templates) for more information.
|
||||
|
||||
For example, change the following:
|
||||
|
||||
```html
|
||||
<!-- Old -->
|
||||
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup">Confirm your mail</a>
|
||||
|
||||
<!-- New -->
|
||||
<a href="{{ .RedirectTo }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup"
|
||||
>Confirm your mail</a
|
||||
>
|
||||
```
|
||||
|
||||
## Mobile deep linking URIs
|
||||
|
||||
For mobile applications you can use deep linking URIs. For example, for your `SITE_URL` you can specify something like `com.supabase://login-callback/` and for additional redirect URLs something like `com.supabase.staging://login-callback/` if needed.
|
||||
|
||||
Read more about deep linking and find code examples for different frameworks [here](/docs/guides/auth/native-mobile-deep-linking).
|
||||
|
||||
## Error Handling
|
||||
|
||||
When authentication fails, the user will still be redirected to the redirect URL provided. However, the error details will be returned as query fragments in the URL. You can parse these query fragments and show a custom error message to the user. For example:
|
||||
|
||||
```js
|
||||
const params = new URLSearchParams(window.location.hash.slice())
|
||||
|
||||
if (params.get('error_code').startsWith('4')) {
|
||||
// show error message if error is a 4xx error
|
||||
window.alert(params.get('error_description'))
|
||||
}
|
||||
```
|
||||
@@ -87,7 +87,7 @@ The [Custom Access Token Auth Hook](/docs/guides/auth/auth-hooks#hook-custom-acc
|
||||
create or replace function public.custom_access_token_hook(event jsonb)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
immutable
|
||||
stable
|
||||
as $$
|
||||
declare
|
||||
claims jsonb;
|
||||
@@ -216,8 +216,7 @@ To utilize Role-Based Access Control (RBAC) in Row Level Security (RLS) policies
|
||||
|
||||
```sql supabase/migrations/init.sql
|
||||
create function public.authorize(
|
||||
requested_permission app_permission,
|
||||
user_id uuid
|
||||
requested_permission app_permission
|
||||
)
|
||||
returns boolean as $$
|
||||
declare
|
||||
@@ -226,7 +225,7 @@ begin
|
||||
select count(*)
|
||||
from public.role_permissions
|
||||
where role_permissions.permission = authorize.requested_permission
|
||||
and role_permissions.role = (auth.jwt() ->> 'user_role')::public.app_role
|
||||
and (role_permissions.role = (select (auth.jwt() ->> 'user_role')::public.app_role))
|
||||
into bind_permissions;
|
||||
|
||||
return bind_permissions > 0;
|
||||
@@ -243,8 +242,8 @@ You can read more about using functions in RLS policies in the [RLS guide](/docs
|
||||
You can then use the `authorize` method within your RLS policies. For example, to enable the desired delete access, you would add the following policies:
|
||||
|
||||
```sql
|
||||
create policy "Allow authorized delete access" on public.channels for delete using ( authorize('channels.delete', auth.uid()) );
|
||||
create policy "Allow authorized delete access" on public.messages for delete using ( authorize('messages.delete', auth.uid()) );
|
||||
create policy "Allow authorized delete access" on public.channels for delete using ( authorize('channels.delete') );
|
||||
create policy "Allow authorized delete access" on public.messages for delete using ( authorize('messages.delete') );
|
||||
```
|
||||
|
||||
## Accessing custom claims in your application
|
||||
|
||||
+12
-1
@@ -129,7 +129,7 @@ CREATE POLICY "View organization settings."
|
||||
ON organization_settings
|
||||
AS RESTRICTIVE
|
||||
USING (
|
||||
sso_provider_id = auth.jwt()#>>'{amr,0,provider}'
|
||||
sso_provider_id = (select auth.jwt()#>>'{amr,0,provider}')
|
||||
);
|
||||
```
|
||||
|
||||
@@ -213,6 +213,17 @@ await supabase.auth.signInWithSSO(
|
||||
|
||||
Calling [`signInWithSSO`](/docs/reference/dart/auth-signinwithsso) starts the sign-in process using the identity provider registered for the `company.com` domain name. It is not required that identity providers be assigned one or multiple domain names, in which case you can use the provider's unique ID instead.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
```swift
|
||||
try await supabase.auth.signInWithSSO(
|
||||
domain: "company.com"
|
||||
)
|
||||
```
|
||||
|
||||
Calling [`signInWithSSO`](/docs/reference/swift/auth-signinwithsso) starts the sign-in process using the identity provider registered for the `company.com` domain name. It is not required that identity providers be assigned one or multiple domain names, in which case you can use the provider's unique ID instead.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -67,11 +67,11 @@ create policy "Public profiles are viewable by everyone."
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
with check ( (select auth.uid()) = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
using ( (select auth.uid()) = id );
|
||||
```
|
||||
|
||||
## Private access
|
||||
@@ -81,7 +81,7 @@ If the data should only be _readable_ by the user who owns the data, we just nee
|
||||
```sql
|
||||
create policy "Profiles are viewable by users who created them."
|
||||
on profiles for select
|
||||
using ( auth.uid() = id );
|
||||
using ( (select auth.uid()) = id );
|
||||
```
|
||||
|
||||
The nice thing about this pattern? We can now query this table via the API and we don't need to include data filters in our API queries - the Policies will handle that for us:
|
||||
@@ -107,6 +107,26 @@ const { data: profile } = await supabase
|
||||
.select('id, username, avatar_url, website')
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
```swift
|
||||
// This will return nothing while the user is logged out
|
||||
let profiles: [Profile] = try await supabase.from("profiles")
|
||||
.select("id, username, avatar_url, website")
|
||||
.execute()
|
||||
.value
|
||||
|
||||
// After the user is logged in, this will only return
|
||||
// the logged-in user's data - in this case a single row
|
||||
try await supabase.auth.signInWithOTP(email: email)
|
||||
let profile: Profile = try await supabase.from("profiles")
|
||||
.select("id, username, avatar_url, website")
|
||||
.single()
|
||||
.execute()
|
||||
.value
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -163,6 +183,20 @@ const { data, error } = await supabase.auth.signUp({
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
```swift
|
||||
try await supabase.auth.signUp(
|
||||
email: "example@email.com",
|
||||
password: "example-password",
|
||||
data: [
|
||||
"first_name": .string("John"),
|
||||
"age": .integer(27),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -198,6 +232,14 @@ const {
|
||||
let metadata = user.user_metadata
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
```swift
|
||||
let user = try await supabase.auth.user()
|
||||
let metadata = user.userMetadata
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: 'Login With Email OTP'
|
||||
description: 'Use Supabase to authenticate your users using one-time password emails.'
|
||||
---
|
||||
|
||||
Email one-time passwords (OTP) are a form of passwordless login where users key in a six digit code sent to their email address to log in to their accounts. By default, a user can only request an OTP once every 60 seconds and they expire after 24 hours.
|
||||
Email one-time passwords (OTP) are a form of passwordless login where users key in a six digit code sent to their email address to log in to their accounts. By default, a user can only request an OTP once every 60 seconds and they expire after 1 hour.
|
||||
|
||||
## Setting up email OTP
|
||||
|
||||
@@ -54,6 +54,18 @@ Future<void> signInWithEmailOtp() async {
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
When your user signs in, call [signInWithOTP()](/docs/reference/swift/auth-signinwithotp) with their email address:
|
||||
|
||||
```swift
|
||||
try await supabase.auth.signInWithOTP(
|
||||
email: "example@email.com",
|
||||
// set this to false if you do not want the user to be automatically signed up
|
||||
shouldCreateUser: false
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -106,6 +118,19 @@ const {
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
Provide an input field for the user to key in the one-time code. To verify the code and complete the user's sign in, call [verifyOTP()](/docs/reference/swift/auth-verifyotp) with their email address, the code, and `type: .email`:
|
||||
|
||||
```swift
|
||||
try await supabase.auth.verifyOTP(
|
||||
email: email,
|
||||
token: "123456",
|
||||
type: .email
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -132,7 +157,7 @@ If successful the user will now be logged in and you should receive a valid sess
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Phone SMS & WhatsApp OTP](/docs/guides/auth/phone-login)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
@@ -4,7 +4,7 @@ title: 'Login With Magic Link'
|
||||
description: 'Use Supabase to authenticate your users using magic links.'
|
||||
---
|
||||
|
||||
Magic links are a form of passwordless logins where users click on a link sent to their email address to log in to their accounts. Magic links only work with email addresses and are one-time use only. By default, a user can only request a magic link once every 60 seconds and they expire after 24 hours.
|
||||
Magic links are a form of passwordless logins where users click on a link sent to their email address to log in to their accounts. Magic links only work with email addresses and are one-time use only. By default, a user can only request a magic link once every 60 seconds and they expire after 1 hour.
|
||||
|
||||
Set up Magic Link logins for your Supabase application.
|
||||
|
||||
@@ -19,6 +19,8 @@ Set up Magic Link logins for your Supabase application.
|
||||
|
||||
When using Magic Links in [the PKCE Flow](/blog/supabase-auth-sso-pkce#introducing-pkce), keep in mind that links can only be opened from the same browser that they are sent from (as defined in the PKCE spec). Consequently, a magic link sent from Chrome on Desktop will be invalid if used on a Mobile Device.
|
||||
|
||||
The same applies for Magic Links in mobile devices. Magic Links can only be opened on the device they are sent from.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Tabs
|
||||
@@ -76,6 +78,19 @@ Future<void> signInWithEmail() async {
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
When your user signs in, call [signInWithOTP()](/docs/reference/swift/auth-signinwithotp) with their email address:
|
||||
|
||||
```swift
|
||||
try await supabase.auth.signInWithOTP(
|
||||
email: "example@email.com",
|
||||
redirectTo: URL(string: "https://example.com/welcome"),
|
||||
// set this to false if you do not want the user to be automatically signed up
|
||||
shouldCreateUser: false
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -119,6 +134,14 @@ Future<void> signOut() async {
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
When your user signs out, call [signOut()](/docs/reference/swift/auth-signout) to remove them from the local storage:
|
||||
|
||||
```swift
|
||||
try await supabase.auth.signOut()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -135,6 +158,6 @@ suspend fun logout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
@@ -91,6 +91,18 @@ await supabase.auth.resetPasswordForEmail('hello@example.com', {
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
Supabase provides a convenient method [`supabase.auth.resetPasswordForEmail`](/docs/reference/swift/auth-resetpasswordforemail) to reset a user password. This method takes a parameter of `redirectTo`, which is an absolute URL to the update password page. This URL must be saved in your allowed [Redirect URLs](https://supabase.com/dashboard/project/_/auth/url-configuration) list found at [Authentication > Redirect Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration).
|
||||
|
||||
```swift
|
||||
try await supabase.auth.resetPasswordForEmail(
|
||||
"hello@example.com",
|
||||
redirectTo: URL(string: "http://example.com/account/update-password")
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -125,13 +137,22 @@ To update the password, call the [`supabase.auth.updateUser`](/docs/reference/ja
|
||||
await supabase.auth.updateUser({ password: new_password })
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
To update the password, call the [`supabase.auth.updateUser`](/docs/reference/swift/auth-updateuser) method with the new password.
|
||||
|
||||
```swift
|
||||
try await supabase.auth.updateUser(user: UserAttributes(password: newPassword))
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
To update the password, call the [`.modifyUser`](/docs/reference/kotlin/auth-updateuser) method with the new password.
|
||||
To update the password, call the [`.updateUser`](/docs/reference/kotlin/auth-updateuser) method with the new password.
|
||||
|
||||
```kotlin
|
||||
supabase.gotrue.modifyUser {
|
||||
supabase.auth.updateUser {
|
||||
password = "new_password"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -80,6 +80,16 @@ const { data, error } = await supabase.auth.signUp({
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
```swift
|
||||
try await supabase.auth.signUp(
|
||||
phone: "+13334445555",
|
||||
password: "some-password"
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -126,6 +136,16 @@ const { user, error } = await supabase.auth.signInWithPassword({
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
```swift
|
||||
try await supabase.auth.signIn(
|
||||
phone: "+13334445555",
|
||||
password: "some-password"
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -171,6 +191,15 @@ const { data, error } = await supabase.auth.signInWithOtp({
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
```swift
|
||||
try await supabase.auth.signInWithOTP(
|
||||
phone: "+13334445555"
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -216,6 +245,17 @@ const { data, error } = await supabase.auth.updateUser({
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
```swift
|
||||
try await supabase.auth.updateUser(
|
||||
user: UserAttributes(
|
||||
phone: "123456789"
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -247,6 +287,19 @@ const {
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOTP`:
|
||||
|
||||
```swift
|
||||
try await supabase.auth.verifyOTP(
|
||||
phone: "+13334445555",
|
||||
token: "123456",
|
||||
type: .sms
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Authenticating users via SMS can become expensive. Adjust your project's rate li
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Preqrequisites
|
||||
## Prerequisites
|
||||
|
||||
You'll need:
|
||||
|
||||
@@ -32,7 +32,7 @@ To set up Twilio Verify, you will need to:
|
||||
When using Twilio Verify, OTPs are generated by Twilio. This means that:
|
||||
|
||||
- Unlike other providers, the OTP expiry duration and message content fields are not configurable via the Supabase dashboard. Please head to Twilio Verify to configure these settings.
|
||||
- The token remains the same during its validity period until the verification is successful. This means if your user make another request within that period, they will receive the same token.
|
||||
- The token remains the same during its validity period until the verification is successful. This means if your user makes another request within that period, they will receive the same token.
|
||||
- Twilio Verify has a separate set of rate limits that apply. Visit Twilio's [Rate Limit and Timeouts page](https://www.twilio.com/docs/verify/api/rate-limits-and-timeouts) to find out more.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
@@ -28,7 +28,7 @@ Policies are easy to understand once you get the hang of them. Each policy is at
|
||||
```sql
|
||||
create policy "Individuals can view their own todos."
|
||||
on todos for select
|
||||
using ( auth.uid() = user_id );
|
||||
using ( (select auth.uid()) = user_id );
|
||||
```
|
||||
|
||||
.. would translate to this whenever a user tries to select from the todos table:
|
||||
@@ -63,6 +63,12 @@ to authenticated
|
||||
using ( true );
|
||||
```
|
||||
|
||||
<Admonition type="note" label="Anonymous user vs the anon key">
|
||||
|
||||
Using the `anon` Postgres role is different from an [anonymous user](/docs/guides/auth/auth-anonymous) in Supabase Auth. An anonymous user assumes the `authenticated` role to access the database and can be differentiated from a permanent user by checking the `is_anonymous` claim in the JWT.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Helper functions
|
||||
|
||||
Supabase provides some helper functions that make it easier to write Policies.
|
||||
@@ -119,6 +125,20 @@ on my_table for select
|
||||
using ( true );
|
||||
```
|
||||
|
||||
### Protect views using `security_invoker`
|
||||
|
||||
Views bypass RLS by default because they are usually created with the `postgres` user. This is a feature of Postgres, which automatically creates views with `security definer`.
|
||||
|
||||
In Postgres 15 and above, you can make a view obey the RLS policies of the underlying tables when invoked by `anon` and `authenticated` roles by setting `security_invoker = true`.
|
||||
|
||||
```sql
|
||||
create view <VIEW_NAME>
|
||||
with(security_invoker = true)
|
||||
as select <QUERY>
|
||||
```
|
||||
|
||||
In older versions of Postgres, protect your views by revoking access from the `anon` and `authenticated` roles, or by putting them in an unexposed schema.
|
||||
|
||||
### Using external authorization systems
|
||||
|
||||
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. If you take this path, don't put your tables in the `public` schema - instead create a new schema for your tables and functions:
|
||||
@@ -172,7 +192,7 @@ create policy "Users can update their own profiles"
|
||||
on profiles for update
|
||||
to authenticated
|
||||
using (
|
||||
auth.uid() = id
|
||||
(select auth.uid()) = id
|
||||
);
|
||||
```
|
||||
|
||||
@@ -202,7 +222,7 @@ alter table teams enable row level security;
|
||||
create policy "Team members can update team details if they belong to the team"
|
||||
on teams
|
||||
for update using (
|
||||
auth.uid() in (
|
||||
(select auth.uid()) in (
|
||||
select user_id from members
|
||||
where team_id = id
|
||||
)
|
||||
@@ -266,7 +286,7 @@ create policy "Only Supabase staff can update the leaderboard"
|
||||
on leaderboard
|
||||
to authenticated
|
||||
for update using (
|
||||
right(auth.jwt() ->> 'email', 13) = '@supabase.com'
|
||||
right((select auth.jwt() ->> 'email'), 13) = '@supabase.com'
|
||||
);
|
||||
```
|
||||
|
||||
@@ -280,7 +300,7 @@ on profiles
|
||||
as restrictive
|
||||
for update
|
||||
to authenticated using (
|
||||
auth.jwt()->>'aal' = 'aal2'
|
||||
(select auth.jwt()->>'aal') = 'aal2'
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ This is traditionally done by setting cookies. Here's an example you can add to
|
||||
|
||||
```typescript
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (event === 'SIGNED_OUT' || event === 'USER_DELETED') {
|
||||
if (event === 'SIGNED_OUT') {
|
||||
// delete cookies on sign out
|
||||
const expires = new Date(0).toUTCString()
|
||||
document.cookie = `my-access-token=; path=/; expires=${expires}; SameSite=Lax; secure`
|
||||
|
||||
@@ -429,12 +429,6 @@ export default function Index () {
|
||||
|
||||
Creating a Supabase client with the `ssr` package automatically configures it to use Cookies. This means your user's session is available throughout the entire SvelteKit stack - page, layout, server, hooks.
|
||||
|
||||
<Admonition type="danger">
|
||||
|
||||
Beware when accessing the session object on the server, because it is not revalidated on every request from the client. That means the sender can tamper with unencoded data in the session object. If you need to verify the integrity of user data for server logic, call `auth.getUser` instead, which will query the Supabase Auth server for trusted user data.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
@@ -475,15 +469,28 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession()`, which returns the session _without_
|
||||
* validating the JWT, this function also calls `getUser()` to validate the
|
||||
* JWT before returning the session.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
if (!session) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) {
|
||||
// JWT validation has failed
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -528,6 +535,11 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
@@ -543,9 +555,12 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
```ts +layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession } }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
|
||||
return {
|
||||
session: await getSession(),
|
||||
session,
|
||||
user,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+2
-1
@@ -79,6 +79,7 @@ Create a new file at `src/routes/auth/confirm/+server.ts` and populate with the
|
||||
|
||||
```ts src/routes/auth/confirm/+server.ts
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
import { type EmailOtpType } from '@supabase/supabase-js'
|
||||
|
||||
export const GET = async (event) => {
|
||||
const {
|
||||
@@ -86,7 +87,7 @@ export const GET = async (event) => {
|
||||
locals: { supabase },
|
||||
} = event
|
||||
const token_hash = url.searchParams.get('token_hash') as string
|
||||
const type = url.searchParams.get('type') as string
|
||||
const type = url.searchParams.get('type') as EmailOtpType | null
|
||||
const next = url.searchParams.get('next') ?? '/'
|
||||
|
||||
if (token_hash && type) {
|
||||
|
||||
@@ -495,7 +495,7 @@ export default async function PrivatePage() {
|
||||
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error || !data?.user) {
|
||||
redirect('/')
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
return <p>Hello {data.user.email}</p>
|
||||
@@ -812,11 +812,11 @@ Since this is an API route, use the Supabase client from `@/utils/supabase/api.t
|
||||
|
||||
<CH.Code>
|
||||
|
||||
```ts pages/api/auth/confirm.tsx
|
||||
```ts pages/api/auth/confirm.ts
|
||||
import { type EmailOtpType } from '@supabase/supabase-js'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import { createClient } from '@/utils/supabase/api'
|
||||
import createClient from '@/utils/supabase/api'
|
||||
|
||||
function stringOrFirstString(item: string | string[] | undefined) {
|
||||
return Array.isArray(item) ? item[0] : item
|
||||
@@ -869,7 +869,7 @@ export default function ErrorPage() {
|
||||
|
||||
If you use dynamic server-side rendering, you can serve a page to authenticated users only by checking for the user data in `getServerSideProps`. Unauthenticated users will be redirected to the home page.
|
||||
|
||||
Since you're calling Supabse from `getServerSideProps`, use the client from `@/utils/supabase/server-props.ts`.
|
||||
Since you're calling Supabase from `getServerSideProps`, use the client from `@/utils/supabase/server-props.ts`.
|
||||
|
||||
<Admonition type="danger">
|
||||
|
||||
|
||||
@@ -207,6 +207,17 @@ app.get("/auth/callback", async function (req, res) {
|
||||
|
||||
Let's point our `.signInWithOAuth` method's redirect to the callback route we create above:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="client"
|
||||
queryGroup="environment"
|
||||
>
|
||||
<TabPanel id="client" label="Client">
|
||||
|
||||
In the browser, `signInWithOAuth` automatically redirects to the SSO provider's authentication endpoint, which then redirects to your endpoint.
|
||||
|
||||
```js
|
||||
await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
@@ -215,3 +226,26 @@ await supabase.auth.signInWithOAuth({
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="server" label="Server">
|
||||
|
||||
In the server, you need to handle the redirect to the OAuth provider's authentication endpoint. The `signInWithOAuth` method returns the endpoint URL, which you can redirect to.
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
options: {
|
||||
redirectTo: 'http://example.com/auth/callback',
|
||||
},
|
||||
})
|
||||
|
||||
if (data.url) {
|
||||
redirect(data.url) // use the redirect API for your server framework
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
</Tabs>
|
||||
@@ -9,15 +9,22 @@ When using Supabase with server-side languages and frameworks - such as Next.js,
|
||||
|
||||
## Framework quickstarts
|
||||
|
||||
<div className="grid cols-[repeat(auto-fit,min-max(250px,1fr)] gap-6 mb-6 not-prose">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-6 mb-6 not-prose">
|
||||
{[
|
||||
{
|
||||
title: 'Next.js',
|
||||
href: '/guides/auth/server-side/nextjs',
|
||||
description:
|
||||
'Automatically configure Supabase to use cookies, making your user and their session available on the client and server.',
|
||||
'Automatically configure Supabase in Next.js to use cookies, making your user and their session available on the client and server.',
|
||||
icon: '/docs/img/icons/nextjs-icon',
|
||||
},
|
||||
{
|
||||
title: 'SvelteKit',
|
||||
href: '/guides/auth/server-side/sveltekit',
|
||||
description:
|
||||
'Automatically configure Supabase in SvelteKit to use cookies, making your user and their session available on the client and server.',
|
||||
icon: '/docs/img/icons/svelte-icon',
|
||||
},
|
||||
].map((item) => {
|
||||
return (
|
||||
<Link href={`${item.href}`} key={item.title} passHref>
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
---
|
||||
title: 'Setting up Server-Side Auth for SvelteKit'
|
||||
hideToc: true
|
||||
---
|
||||
|
||||
Set up Server-Side Auth to use cookie-based authentication with SvelteKit.
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
|
||||
<StepHikeCompact.Details title="Install Supabase packages">
|
||||
|
||||
Install the `@supabase/supabase-js` package and the helper `@supabase/ssr` package.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```sh
|
||||
npm install @supabase/supabase-js @supabase/ssr
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
|
||||
<StepHikeCompact.Details title="Set up environment variables">
|
||||
|
||||
Create a `.env.local` file in your project root directory.
|
||||
|
||||
Fill in your `PUBLIC_SUPABASE_URL` and `PUBLIC_SUPABASE_ANON_KEY`:
|
||||
|
||||
<ProjectConfigVariables variable="url" />
|
||||
<ProjectConfigVariables variable="anonKey" />
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```txt .env.local
|
||||
PUBLIC_SUPABASE_URL=<your_supabase_project_url>
|
||||
PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
|
||||
<StepHikeCompact.Details title="Set up server-side hooks">
|
||||
|
||||
Set up server-side hooks in `src/hooks.server.ts`. The hooks:
|
||||
|
||||
- Create a request-specific Supabase client, using the user credentials from the request cookie. This client is used for server-only code.
|
||||
- Check user authentication.
|
||||
- Guard protected pages.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts src/hooks.server.ts
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { type Handle, redirect } from '@sveltejs/kit'
|
||||
import { sequence } from '@sveltejs/kit/hooks'
|
||||
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
|
||||
|
||||
const supabase: Handle = async ({ event, resolve }) => {
|
||||
/**
|
||||
* Creates a Supabase client specific to this server request.
|
||||
*
|
||||
* The Supabase client gets the Auth token from the request cookies.
|
||||
*/
|
||||
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
cookies: {
|
||||
get: (key) => event.cookies.get(key),
|
||||
/**
|
||||
* SvelteKit's cookies API requires `path` to be explicitly set in
|
||||
* the cookie options. Setting `path` to `/` replicates previous/
|
||||
* standard behavior.
|
||||
*/
|
||||
set: (key, value, options) => {
|
||||
event.cookies.set(key, value, { ...options, path: '/' })
|
||||
},
|
||||
remove: (key, options) => {
|
||||
event.cookies.delete(key, { ...options, path: '/' })
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Unlike `supabase.auth.getSession()`, which returns the session _without_
|
||||
* validating the JWT, this function also calls `getUser()` to validate the
|
||||
* JWT before returning the session.
|
||||
*/
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
if (!session) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) {
|
||||
// JWT validation has failed
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
filterSerializedResponseHeaders(name) {
|
||||
/**
|
||||
* Supabase libraries use the `content-range` header, so we need to
|
||||
* tell SvelteKit to pass it through.
|
||||
*/
|
||||
return name === 'content-range'
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const authGuard: Handle = async ({ event, resolve }) => {
|
||||
const { session, user } = await event.locals.safeGetSession()
|
||||
event.locals.session = session
|
||||
event.locals.user = user
|
||||
|
||||
if (!event.locals.session && event.url.pathname.startsWith('/private')) {
|
||||
return redirect(303, '/auth')
|
||||
}
|
||||
|
||||
if (event.locals.session && event.url.pathname === '/auth') {
|
||||
return redirect(303, '/private')
|
||||
}
|
||||
|
||||
return resolve(event)
|
||||
}
|
||||
|
||||
export const handle: Handle = sequence(supabase, authGuard)
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
|
||||
<StepHikeCompact.Details title="Create TypeScript definitions">
|
||||
|
||||
To prevent TypeScript errors, add type definitions for the new `event.locals` properties.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts src/app.d.ts
|
||||
import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
supabase: SupabaseClient
|
||||
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
|
||||
session: Session | null
|
||||
user: User | null
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
|
||||
<StepHikeCompact.Details title="Create a Supabase client in your root layout">
|
||||
|
||||
Create a Supabase client in your root `+layout.ts`. This client can be used to access Supabase from the client or the server. In order to get access to the Auth token on the server, use a `+layout.server.ts` file to pass in the session from `event.locals`.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
<CH.Code>
|
||||
|
||||
```ts src/routes/+layout.ts
|
||||
import { createBrowserClient, createServerClient, isBrowser, parse } from '@supabase/ssr'
|
||||
|
||||
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
|
||||
|
||||
import type { LayoutLoad } from './$types'
|
||||
|
||||
export const load: LayoutLoad = async ({ data, depends, fetch }) => {
|
||||
/**
|
||||
* Declare a dependency so the layout can be invalidated, for example, on
|
||||
* session refresh.
|
||||
*/
|
||||
depends('supabase:auth')
|
||||
|
||||
const supabase = isBrowser()
|
||||
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch,
|
||||
},
|
||||
cookies: {
|
||||
get(key) {
|
||||
const cookie = parse(document.cookie)
|
||||
return cookie[key]
|
||||
},
|
||||
},
|
||||
})
|
||||
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch,
|
||||
},
|
||||
cookies: {
|
||||
get() {
|
||||
return JSON.stringify(data.session)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
return { session, supabase, user }
|
||||
}
|
||||
```
|
||||
|
||||
```ts src/routes/+layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { session } }) => {
|
||||
return {
|
||||
session,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</CH.Code>
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={6}>
|
||||
|
||||
<StepHikeCompact.Details title="Listen to Auth events">
|
||||
|
||||
Set up a listener for Auth events on the client, to handle session refreshes and signouts.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```svelte src/routes/+layout.svelte
|
||||
<script>
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let data;
|
||||
$: ({ session, supabase } = data);
|
||||
|
||||
onMount(() => {
|
||||
const { data } = supabase.auth.onAuthStateChange((_, newSession) => {
|
||||
if (!newSession) {
|
||||
/**
|
||||
* Queue this as a task so the navigation won't prevent the
|
||||
* triggering function from completing
|
||||
*/
|
||||
setTimeout(() => {
|
||||
goto('/', { invalidateAll: true });
|
||||
});
|
||||
}
|
||||
if (newSession?.expires_at !== session?.expires_at) {
|
||||
invalidate('supabase:auth');
|
||||
}
|
||||
});
|
||||
|
||||
return () => data.subscription.unsubscribe();
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={7}>
|
||||
|
||||
<StepHikeCompact.Details title="Create your first page">
|
||||
|
||||
Create your first page. This example page calls Supabase from the server to get a list of countries from the database.
|
||||
|
||||
This is an example of a public page that uses publicly readable data.
|
||||
|
||||
To populate your database, run the [countries quickstart](/dashboard/project/_/sql/quickstarts) from your dashboard.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
<CH.Code>
|
||||
|
||||
```ts src/routes/+page.server.ts
|
||||
import type { PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ locals: { supabase } }) => {
|
||||
const { data: countries } = await supabase.from('countries').select('name').limit(5).order('name')
|
||||
return { countries: countries ?? [] }
|
||||
}
|
||||
```
|
||||
|
||||
```svelte src/routes/+page.svelte
|
||||
<script>
|
||||
export let data;
|
||||
$: ({ countries } = data);
|
||||
</script>
|
||||
|
||||
<h1>Welcome to Supabase!</h1>
|
||||
<ul>
|
||||
{#each countries as country}
|
||||
<li>{country.name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
```
|
||||
|
||||
</CH.Code>
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={8}>
|
||||
|
||||
<StepHikeCompact.Details title="Change the Auth confirmation path">
|
||||
|
||||
If you have email confirmation turned on (the default), a new user will receive an email confirmation after signing up.
|
||||
|
||||
Change the email template to support a server-side authentication flow.
|
||||
|
||||
Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In the `Confirm signup` template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/api/auth/confirm?token_hash={{ .TokenHash }}&type=signup`.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={9}>
|
||||
|
||||
<StepHikeCompact.Details title="Create a login page">
|
||||
|
||||
Next, create a login page to let users sign up and log in.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
<CH.Code>
|
||||
|
||||
```ts src/routes/auth/+page.server.ts
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
import type { Actions } from './$types'
|
||||
|
||||
export const actions: Actions = {
|
||||
signup: async ({ request, locals: { supabase } }) => {
|
||||
const formData = await request.formData()
|
||||
const email = formData.get('email') as string
|
||||
const password = formData.get('password') as string
|
||||
|
||||
const { error } = await supabase.auth.signUp({ email, password })
|
||||
if (error) {
|
||||
console.error(error)
|
||||
return redirect(303, '/auth/error')
|
||||
} else {
|
||||
return redirect(303, '/')
|
||||
}
|
||||
},
|
||||
login: async ({ request, locals: { supabase } }) => {
|
||||
const formData = await request.formData()
|
||||
const email = formData.get('email') as string
|
||||
const password = formData.get('password') as string
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword({ email, password })
|
||||
if (error) {
|
||||
console.error(error)
|
||||
return redirect(303, '/auth/error')
|
||||
} else {
|
||||
return redirect(303, '/private')
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```svelte src/routes/auth/+page.svelte
|
||||
<form method="POST" action="?/login">
|
||||
<label>
|
||||
Email
|
||||
<input name="email" type="email" />
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input name="password" type="password" />
|
||||
</label>
|
||||
<button>Login</button>
|
||||
<button formaction="?/signup">Sign up</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
```svelte src/routes/auth/+layout.svelte
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
</nav>
|
||||
</header>
|
||||
<slot />
|
||||
```
|
||||
|
||||
```svelte src/routes/auth/error/+page.svelte
|
||||
<p>Login error</p>
|
||||
```
|
||||
|
||||
</CH.Code>
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={10}>
|
||||
|
||||
<StepHikeCompact.Details title="Create the signup confirmation route">
|
||||
|
||||
Finish the signup flow by creating the API route to handle email verification.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts src/routes/api/auth/confirm/+server.ts
|
||||
import type { EmailOtpType } from '@supabase/supabase-js'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
import type { RequestHandler } from './$types'
|
||||
|
||||
export const GET: RequestHandler = async ({ url, locals: { supabase } }) => {
|
||||
const token_hash = url.searchParams.get('token_hash')
|
||||
const type = url.searchParams.get('type') as EmailOtpType | null
|
||||
const next = url.searchParams.get('next') ?? '/'
|
||||
|
||||
/**
|
||||
* Clean up the redirect URL by deleting the Auth flow parameters.
|
||||
*
|
||||
* `next` is preserved for now, because it's needed in the error case.
|
||||
*/
|
||||
const redirectTo = new URL(url)
|
||||
redirectTo.pathname = next
|
||||
redirectTo.searchParams.delete('token_hash')
|
||||
redirectTo.searchParams.delete('type')
|
||||
|
||||
if (token_hash && type) {
|
||||
const { error } = await supabase.auth.verifyOtp({ type, token_hash })
|
||||
if (!error) {
|
||||
redirectTo.searchParams.delete('next')
|
||||
return redirect(303, redirectTo)
|
||||
}
|
||||
}
|
||||
|
||||
redirectTo.pathname = '/auth/error'
|
||||
return redirect(303, redirectTo)
|
||||
}
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={11}>
|
||||
|
||||
<StepHikeCompact.Details title="Create private routes">
|
||||
|
||||
Create private routes that can only be accessed by authenticated users. The routes in the `private` directory are protected by the route guard in `hooks.server.ts`.
|
||||
|
||||
To ensure that `hooks.server.ts` runs for every nested path, put a `+layout.server.ts` file in the `private` directory. This file can be empty, but must exist to protect routes that don't have their own `+layout|page.server.ts`.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
<CH.Code className="min-h-[30rem]">
|
||||
|
||||
```ts src/routes/private/+layout.server.ts
|
||||
/**
|
||||
* This file is necessary to ensure protection of all routes in the `private`
|
||||
* directory. It makes the routes in this directory _dynamic_ routes, which
|
||||
* send a server request, and thus trigger `hooks.server.ts`.
|
||||
**/
|
||||
```
|
||||
|
||||
```svelte src/routes/private/+layout.svelte
|
||||
<script>
|
||||
export let data;
|
||||
$: ({ supabase } = data);
|
||||
|
||||
$: logout = async () => {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
</nav>
|
||||
<button on:click={logout}>Logout</button>
|
||||
</header>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
```
|
||||
|
||||
```sql SQL
|
||||
-- Run this SQL against your database to create a `notes` table.
|
||||
|
||||
create table notes (
|
||||
id bigint primary key generated always as identity,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
user_id uuid references auth.users on delete cascade not null default auth.uid(),
|
||||
note text not null
|
||||
);
|
||||
|
||||
alter table notes enable row level security;
|
||||
|
||||
revoke all on table notes from authenticated;
|
||||
revoke all on table notes from anon;
|
||||
|
||||
grant all (note) on table notes to authenticated;
|
||||
grant select (id) on table notes to authenticated;
|
||||
grant delete on table notes to authenticated;
|
||||
|
||||
create policy "Users can access and modify their own notes"
|
||||
on notes
|
||||
for all
|
||||
to authenticated
|
||||
using ((select auth.uid()) = user_id);
|
||||
```
|
||||
|
||||
```svelte src/routes/private/+page.server.ts
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ depends, locals: { supabase } }) => {
|
||||
depends('supabase:db:notes');
|
||||
const { data: notes } = await supabase.from('notes').select('id,note').order('id');
|
||||
return { notes: notes ?? [] };
|
||||
};
|
||||
```
|
||||
|
||||
```svelte src/routes/private/+page.svelte
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import type { EventHandler } from 'svelte/elements';
|
||||
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
$: ({ notes, supabase, user } = data);
|
||||
|
||||
let handleSubmit: EventHandler<SubmitEvent, HTMLFormElement>;
|
||||
$: handleSubmit = async (evt) => {
|
||||
evt.preventDefault();
|
||||
if (!evt.target) return;
|
||||
|
||||
const form = evt.target as HTMLFormElement;
|
||||
|
||||
const note = (new FormData(form).get('note') ?? '') as string;
|
||||
if (!note) return;
|
||||
|
||||
const { error } = await supabase.from('notes').insert({ note });
|
||||
if (error) console.error(error);
|
||||
|
||||
invalidate('supabase:db:notes');
|
||||
form.reset();
|
||||
};
|
||||
</script>
|
||||
|
||||
<h1>Private page for user: {user?.email}</h1>
|
||||
<h2>Notes</h2>
|
||||
<ul>
|
||||
{#each notes as note}
|
||||
<li>{note.note}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<form on:submit={handleSubmit}>
|
||||
<label>
|
||||
Add a note
|
||||
<input name="note" type="text" />
|
||||
</label>
|
||||
</form>
|
||||
```
|
||||
|
||||
</CH.Code>
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
@@ -112,6 +112,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Bitbucket Account](https://bitbucket.org)
|
||||
@@ -113,7 +113,7 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Discord Account](https://discord.com)
|
||||
- [Discord Developer Portal](https://discord.com/developers)
|
||||
@@ -131,6 +131,6 @@ You can read more about App Review [here](https://developers.facebook.com/docs/a
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Facebook Developers Dashboard](https://developers.facebook.com/)
|
||||
@@ -112,6 +112,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Figma Developers page](https://www.figma.com/developers)
|
||||
@@ -123,6 +123,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [GitHub Developer Settings](https://github.com/settings/developers)
|
||||
@@ -109,6 +109,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [GitLab Account](https://gitlab.com)
|
||||
@@ -311,7 +311,7 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf
|
||||
|
||||
## Google sign-in for Web, macOS, Windows, and Linux
|
||||
|
||||
Google sign-in with Supabase on Web, macOS, Windows, and Linux is done through the [signInWithOAuth](docs/reference/dart/auth-signinwithoauth) method.
|
||||
Google sign-in with Supabase on Web, macOS, Windows, and Linux is done through the [signInWithOAuth](/docs/reference/dart/auth-signinwithoauth) method.
|
||||
|
||||
This method of signing in is web based, and will open a browser window to perform the sign in. For non-web platforms, the user is brought back to the app via [deep linking](/docs/guides/auth/native-mobile-deep-linking?platform=flutter).
|
||||
|
||||
|
||||
@@ -135,6 +135,6 @@ Do reach out to support if you have any concerns around this change.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [LinkedIn Developer Dashboard](https://api.LinkedIn.com/apps)
|
||||
@@ -112,7 +112,7 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Notion Account](https://notion.so)
|
||||
- [Notion Developer Portal](https://www.notion.so/my-integrations)
|
||||
@@ -130,6 +130,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Slack Developer Dashboard](https://api.slack.com/apps)
|
||||
@@ -124,6 +124,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/)
|
||||
@@ -127,7 +127,7 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Twitch Account](https://twitch.tv)
|
||||
- [Twitch Developer Console](https://dev.twitch.tv/console)
|
||||
@@ -118,6 +118,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Twitter Developer Dashboard](https://developer.twitter.com/en/portal/dashboard)
|
||||
@@ -125,6 +125,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Zoom App Marketplace](https://marketplace.zoom.us/)
|
||||
@@ -115,4 +115,10 @@ Contains the new user's email address. This is only available in the `email_chan
|
||||
|
||||
## Deploying email templates
|
||||
|
||||
These settings are only for local development. To update your hosted project, please copy the templates into the [Email Templates](/dashboard/project/_/auth/templates) section of the Dashboard.
|
||||
These settings are for local development. To apply the changes locally, stop and restart the Supabase containers:
|
||||
|
||||
```sh
|
||||
supabase stop && supabase start
|
||||
```
|
||||
|
||||
For hosted projects managed by Supabase, copy the templates into the [Email Templates](/dashboard/project/_/auth/templates) section of the Dashboard.
|
||||
@@ -461,7 +461,17 @@ The buckets and objects themselves are rows in the storage schema so they won't
|
||||
|
||||
### Local logging
|
||||
|
||||
Local logs rely on the Supabase Analytics Server, and are available in the Studio automatically.
|
||||
Local logs rely on the Supabase Analytics Server, and are not enabled by default. To enable this, edit your `supabase/config.toml` file and set `enabled` to `true`.
|
||||
|
||||
```bash supabase/config.toml
|
||||
[analytics]
|
||||
# Set enabled to true to see logs in the Logs Explorer
|
||||
enabled = true
|
||||
port = 54327
|
||||
vector_port = 54328
|
||||
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||
backend = "postgres"
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Usage:
|
||||
supabase test db [flags]
|
||||
```
|
||||
|
||||
This is powered by the [pgTAP](https://supabase.com/docs/guides/database/extensions/pgtap) extension. You can find a full guide to writing an running tests in the [Testing your database](/docs/guides/database/testing) section.
|
||||
This is powered by the [pgTAP](https://supabase.com/docs/guides/database/extensions/pgtap) extension. You can find a full guide to writing and running tests in the [Testing your database](/docs/guides/database/testing) section.
|
||||
|
||||
### Test helpers
|
||||
|
||||
@@ -28,7 +28,7 @@ Our friends at [Basejump](https://usebasejump.com/) have created a useful set of
|
||||
|
||||
### Running database tests in CI
|
||||
|
||||
Use our GitHub Action to [automate your database tests](docs/guides/cli/github-action/testing#testing-your-database).
|
||||
Use our GitHub Action to [automate your database tests](/docs/guides/cli/github-action/testing#testing-your-database).
|
||||
|
||||
## Testing your Edge Functions
|
||||
|
||||
|
||||
@@ -72,6 +72,30 @@ const { data, error } = await supabase
|
||||
.insert([{ id: 2, textarray: ['one', 'two', 'three', 'four'] }])
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
Insert a record from the Swift client:
|
||||
|
||||
```swift
|
||||
struct ArrayTest: Encodable {
|
||||
let id: Int
|
||||
let textarray: [String]
|
||||
}
|
||||
|
||||
try await supabase
|
||||
.from("arraytest")
|
||||
.insert(
|
||||
[
|
||||
ArrayTest(
|
||||
id: 2,
|
||||
textarray: ["one", "two", "three", "four"]
|
||||
)
|
||||
]
|
||||
)
|
||||
.execute()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -156,11 +180,35 @@ returns:
|
||||
]
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
This returns the entire array field:
|
||||
|
||||
```swift
|
||||
struct Response: Decodable {
|
||||
let textarray: [String]
|
||||
}
|
||||
|
||||
let response: [Response] = try await supabase.from("arraytest").select("textarray").execute().value
|
||||
dump(response)
|
||||
```
|
||||
|
||||
returns:
|
||||
|
||||
```
|
||||
[
|
||||
Response(
|
||||
textarray: ["Harry", "Larry", "Moe"],
|
||||
)
|
||||
]
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [PostgreSQL Arrays](https://www.postgresql.org/docs/15/arrays.html)
|
||||
@@ -374,3 +374,36 @@ Connection pooler logs are found [here](https://supabase.com/dashboard/project/_
|
||||
|
||||
- Terminating with reason :client_termination when state was :busy
|
||||
This error happens when the client terminates the connection before the connection with the database is completed.
|
||||
|
||||
## Finding your database hostname
|
||||
|
||||
Your database’s hostname is crucial for establishing a direct connection. It resolves to the underlying IP address of your database. To find your hostname, navigate to your [Database Settings](/dashboard/project/_/settings/database).
|
||||
|
||||
Note that the pooler (connection pooler) has a different IP than your database. To reveal the database host and direct connection string, hide the pooler connection string.
|
||||
|
||||
Example hostname: `db.<PROJECT_REF>.supabase.co`
|
||||
|
||||
## Managing your IP address
|
||||
|
||||
To determine your current IP address, you can use an IP address [lookup website](https://whatismyipaddress.com/hostname-ip) or the terminal command:
|
||||
|
||||
```shell
|
||||
nslookup db.<PROJECT_REF>.supabase.co
|
||||
```
|
||||
|
||||
This command queries the domain name servers to find the IP address of the given hostname.
|
||||
|
||||
Example IPv6 Address: `2a05:d014:1c06:5f0c:d7a9:8616:bee2:30df`
|
||||
|
||||
### IPv6 address
|
||||
|
||||
Upon project creation, a static IPv6 address is assigned. This address might change when:
|
||||
|
||||
- A project is paused or resumed.
|
||||
- The database is upgraded.
|
||||
|
||||
### IPv4 address
|
||||
|
||||
Opting for the static [IPv4 add-on](https://supabase.com/docs/guides/platform/ipv4-address) provides a more stable connection address. The IPv4 address remains constant unless the project is paused or resumed.
|
||||
|
||||
Unlike the IPv6 address, upgrading your database doesn't affect the IPv4 address.
|
||||
@@ -0,0 +1,157 @@
|
||||
---
|
||||
id: 'serverless-drivers'
|
||||
title: 'Serverless Drivers'
|
||||
description: 'Connecting to your Postgres database in serverless environments.'
|
||||
subtitle: 'Connecting to your Postgres database in serverless environments.'
|
||||
---
|
||||
|
||||
Supabase provides several options for connecting to your Postgres database from serverless environments.
|
||||
|
||||
[supabase-js](/reference/javascript/introduction) is an isomorphic JavaScript client that uses the [auto-generated REST API](/guides/api) and therefore works in any environment that supports HTTPS connections. This API has a built-in [connection pooler](/guides/database/connecting-to-postgres#connection-pooler) and can serve thousands of simultaneous requests, and therefore is ideal for Serverless workloads.
|
||||
|
||||
## Vercel Edge Functions
|
||||
|
||||
Vercel's [Edge runtime](https://vercel.com/docs/functions/runtimes/edge-runtime) is built on top of the [V8 engine](https://v8.dev/), that provides a limited set of Web Standard APIs.
|
||||
|
||||
### Quickstart
|
||||
|
||||
Choose one of these Vercel Deploy Templates which use our [Vercel Deploy Integration](https://vercel.com/integrations/supabase) to automatically configure your connection strings as environment variables on your Vercel project!
|
||||
|
||||
<div>
|
||||
<div className="grid grid-cols-12 gap-6 not-prose">
|
||||
{[
|
||||
{
|
||||
title: 'supabase-js',
|
||||
hasLightIcon: true,
|
||||
href: 'https://supabase.link/nextjs-with-supabase-starter',
|
||||
description: 'A Next.js App Router template configured with cookie-based auth using Supabase, TypeScript and Tailwind CSS.'
|
||||
},
|
||||
/* { TODO: Link the correct next.js template that uses drizzle ORM with supabase database.
|
||||
hasLightIcon: true,
|
||||
href: 'https://supabase.link/nextjs-supabase-drizzle',
|
||||
description: "Simple Next.js template that uses Supabase as the database and Drizzle as the ORM.",
|
||||
},*/
|
||||
{
|
||||
title: 'Kysely',
|
||||
hasLightIcon: true,
|
||||
href: 'https://supabase.link/nextjs-supabase-kysely',
|
||||
description: 'Simple Next.js template that uses Supabase as the database and Kysely as the query builder.',
|
||||
},
|
||||
/* { TODO: figure out how to get around Prisma accelerate requirement...
|
||||
title: 'Prisma',
|
||||
hasLightIcon: true,
|
||||
href: 'https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fstorage%2Fpostgres-prisma&project-name=postgres-prisma&repository-name=postgres-prisma&demo-title=Vercel%20Postgres%20%2B%20Prisma%20Next.js%20Starter&demo-description=Simple%20Next.js%20template%20that%20uses%20Vercel%20Postgres%20as%20the%20database%20and%20Prisma%20as%20the%20ORM.&demo-url=https%3A%2F%2Fpostgres-prisma.vercel.app%2F&demo-image=https%3A%2F%2Fpostgres-prisma.vercel.app%2Fopengraph-image.png&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6',
|
||||
description: 'Simple Next.js template that uses Vercel Postgres as the database and Prisma as the ORM.',
|
||||
} */
|
||||
].map((resource) => {
|
||||
return (
|
||||
<Link
|
||||
href={`${resource.href}`}
|
||||
key={resource.title}
|
||||
className={'col-span-12 md:col-span-4'}
|
||||
passHref
|
||||
>
|
||||
<GlassPanel {...resource} background={false} showIconBg={true}>
|
||||
{resource.description}
|
||||
</GlassPanel>
|
||||
</Link>
|
||||
)
|
||||
|
||||
})}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### Manual configuration
|
||||
|
||||
In your [`Database Settings`](https://supabase.com/dashboard/project/_/settings/database), make sure `Use connection pooler` is checked and `Transaction` mode is selected, then copy the URI and save it as the `POSTGRES_URL` environment variable. Remember to replace the password placeholder with your actual database password and add the following suffix `?workaround=supabase-pooler.vercel`.
|
||||
|
||||
```txt .env.local
|
||||
POSTGRES_URL="postgres://postgres.cfcxynqnhdybqtbhjemm:[YOUR-PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?workaround=supabase-pooler.vercel"
|
||||
```
|
||||
|
||||
<Tabs scrollable defaultActiveId="drizzle" type="underlined" size="small">
|
||||
|
||||
<TabPanel id="drizzle" label="Drizzle">
|
||||
|
||||
```ts lib/drizzle.ts
|
||||
import { pgTable, serial, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'
|
||||
import { InferSelectModel, InferInsertModel } from 'drizzle-orm'
|
||||
import { sql } from '@vercel/postgres'
|
||||
import { drizzle } from 'drizzle-orm/vercel-postgres'
|
||||
|
||||
export const UsersTable = pgTable(
|
||||
'users',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
email: text('email').notNull(),
|
||||
image: text('image').notNull(),
|
||||
createdAt: timestamp('createdAt').defaultNow().notNull(),
|
||||
},
|
||||
(users) => {
|
||||
return {
|
||||
uniqueIdx: uniqueIndex('unique_idx').on(users.email),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export type User = InferSelectModel<typeof UsersTable>
|
||||
export type NewUser = InferInsertModel<typeof UsersTable>
|
||||
|
||||
// Connect to Vercel Postgres
|
||||
export const db = drizzle(sql)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kysely" label="Kysely">
|
||||
|
||||
```ts lib/kysely.ts
|
||||
import { Generated, ColumnType } from 'kysely'
|
||||
import { createKysely } from '@vercel/postgres-kysely'
|
||||
|
||||
interface UserTable {
|
||||
// Columns that are generated by the database should be marked
|
||||
// using the `Generated` type. This way they are automatically
|
||||
// made optional in inserts and updates.
|
||||
id: Generated<number>
|
||||
name: string
|
||||
email: string
|
||||
image: string
|
||||
|
||||
// You can specify a different type for each operation (select, insert and
|
||||
// update) using the `ColumnType<SelectType, InsertType, UpdateType>`
|
||||
// wrapper. Here we define a column `createdAt` that is selected as
|
||||
// a `Date`, can optionally be provided as a `string` in inserts and
|
||||
// can never be updated:
|
||||
createdAt: ColumnType<Date, string | undefined, never>
|
||||
}
|
||||
|
||||
// Keys of this interface are table names.
|
||||
export interface Database {
|
||||
users: UserTable
|
||||
}
|
||||
|
||||
export const db = createKysely<Database>()
|
||||
export { sql } from 'kysely'
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Cloudflare Workers
|
||||
|
||||
Cloudflare's Workers runtime also uses the [V8 engine](https://v8.dev/) but provides polyfills for a subset of Node.js APIs and [TCP Sockets API](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/), giving you a couple of options:
|
||||
|
||||
- [supabase-js](https://developers.cloudflare.com/workers/databases/native-integrations/supabase/)
|
||||
- [Postgres.js](https://github.com/porsager/postgres?tab=readme-ov-file#cloudflare-workers-support)
|
||||
- [node-postgres](https://developers.cloudflare.com/workers/tutorials/postgres/)
|
||||
|
||||
## Supabase Edge Functions
|
||||
|
||||
Supabase Edge Functions uses the [Deno runtime](https://deno.com/) which has native support for TCP connections allowing you to choose your favorite client:
|
||||
|
||||
- [supabase-js](/docs/guides/functions/connect-to-postgres#using-supabase-js)
|
||||
- [Deno Postgres driver](/docs/guides/functions/connect-to-postgres#using-a-postgres-client)
|
||||
- [Postgres.js](https://github.com/porsager/postgres)
|
||||
- [Drizzle](/docs/guides/functions/connect-to-postgres#using-drizzle)
|
||||
@@ -4,7 +4,20 @@ title: 'index_advisor: query optimization'
|
||||
description: 'Automatically optimize SQL queries'
|
||||
---
|
||||
|
||||
[Index advisor](https://database.dev/olirice/index_advisor) is a Postgres extension for recommending indexes to improve query performance.
|
||||
[Index advisor](https://github.com/supabase/index_advisor) is a Postgres extension for recommending indexes to improve query performance.
|
||||
|
||||
Features:
|
||||
|
||||
- Supports generic parameters e.g. `$1`, `$2`
|
||||
- Supports materialized views
|
||||
- Identifies tables/columns obfuscated by views
|
||||
- Skips duplicate indexes
|
||||
|
||||
index_advisor is accessible directly through Supabase Studio by navigating to the [Query Performance Report](/dashboard/project/*/database/query-performance) and selecting a query and then the "indexes" tab.
|
||||
|
||||

|
||||
|
||||
Alternatively, you can use index_advisor directly via SQL.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -20,25 +33,12 @@ from
|
||||
(1 row)
|
||||
```
|
||||
|
||||
Features:
|
||||
|
||||
- Supports generic parameters e.g. `$1`, `$2`
|
||||
- Supports materialized views
|
||||
- Identifies tables/columns obfuscated by views
|
||||
- Skips duplicate indexes
|
||||
|
||||
## Installation
|
||||
|
||||
index_advisor is a trusted language extension, which means it is directly installable by users from the [database.dev](https://database.dev/) SQL package repository.
|
||||
|
||||
To get started, enable the dbdev client by executing the [setup SQL script](https://database.dev/installer).
|
||||
|
||||
Then, install index_advisor by running
|
||||
To get started, enable index_advisor by running
|
||||
|
||||
```sql
|
||||
select dbdev.install('olirice-index_advisor');
|
||||
create extension if not exists hypopg;
|
||||
create extension "olirice-index_advisor";
|
||||
create extension index_advisor;
|
||||
```
|
||||
|
||||
## API
|
||||
@@ -152,5 +152,4 @@ from
|
||||
|
||||
## Resources
|
||||
|
||||
- dbdev [`index_advisor`](https://database.dev/olirice/index_advisor) docs
|
||||
- dbdev [Github Repository](https://github.com/supabase/dbdev)
|
||||
- [`index_advisor`](https://github.com/supabase/index_advisor) repo
|
||||
@@ -62,11 +62,13 @@ The schedule uses the standard cron syntax, in which \* means "run every time pe
|
||||
* * * * *
|
||||
```
|
||||
|
||||
You can use [crontab.guru](https://crontab.guru/) to help validate your cron schedules.
|
||||
You can use `[1-59] seconds` as the cron syntax to schedule sub-minute jobs. This is available on `pg_cron` v1.5.0+; upgrade your existing Supabase project to use this syntax.
|
||||
|
||||
Head over to [crontab.guru](https://crontab.guru/) to validate your cron schedules.
|
||||
|
||||
### Scheduling system maintenance
|
||||
|
||||
Be extremely careful when setting up pg_cron jobs for system maintenance tasks as they can have unintended consequences. For instance, scheduling a command to terminate idle connections with `pg_terminate_backend(pid)` can disrupt critical background processes like nightly backups. Often, there is an existing Postgres setting e.g. `idle_session_timeout` that can perform these common maintenance tasks without the risk.
|
||||
Be extremely careful when setting up `pg_cron` jobs for system maintenance tasks as they can have unintended consequences. For instance, scheduling a command to terminate idle connections with `pg_terminate_backend(pid)` can disrupt critical background processes like nightly backups. Often, there is an existing Postgres setting e.g. `idle_session_timeout` that can perform these common maintenance tasks without the risk.
|
||||
|
||||
Reach out to [Supabase Support](https://supabase.com/support) if you're unsure if that applies to your use case.
|
||||
|
||||
@@ -74,7 +76,7 @@ Reach out to [Supabase Support](https://supabase.com/support) if you're unsure i
|
||||
|
||||
### Delete data every week
|
||||
|
||||
Delete old data on Saturday at 3:30am (GMT):
|
||||
Delete old data every Saturday at 3:30am (GMT):
|
||||
|
||||
```sql
|
||||
select cron.schedule (
|
||||
@@ -86,21 +88,35 @@ select cron.schedule (
|
||||
|
||||
### Run a vacuum every day
|
||||
|
||||
Vacuum every day at 3:00am (GMT)
|
||||
Vacuum every day at 3:00am (GMT):
|
||||
|
||||
```sql
|
||||
select cron.schedule('nightly-vacuum', '0 3 * * *', 'VACUUM');
|
||||
```
|
||||
|
||||
### Invoke Supabase Edge Function every minute
|
||||
### Call a database function every 5 minutes
|
||||
|
||||
Make a POST request to a Supabase Edge Function every minute. Note: this requires the [`pg_net` extension](/docs/guides/database/extensions/pgnet) to be enabled.
|
||||
Create a [`hello_world()`](https://supabase.com/docs/guides/database/functions?language=sql#simple-functions) database function and then call it every 5 minutes:
|
||||
|
||||
```sql
|
||||
select cron.schedule('call-db-function', '*/5 * * * *', 'CALL hello_world()');
|
||||
```
|
||||
|
||||
### Invoke Supabase Edge Function every 30 seconds
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
This requires `pg_cron` v1.5.0+ and the [`pg_net` extension](/docs/guides/database/extensions/pgnet) to be enabled.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Make a POST request to a Supabase Edge Function every 30 seconds:
|
||||
|
||||
```sql
|
||||
select
|
||||
cron.schedule(
|
||||
'invoke-function-every-minute',
|
||||
'* * * * *', -- every minute
|
||||
'invoke-function-every-half-minute',
|
||||
'30 seconds',
|
||||
$$
|
||||
select
|
||||
net.http_post(
|
||||
@@ -164,4 +180,4 @@ The records in cron.job_run_details are not cleaned automatically which will tak
|
||||
|
||||
## Resources
|
||||
|
||||
- [pg_cron GitHub Repository](https://github.com/citusdata/pg_cron)
|
||||
- [`pg_cron` GitHub Repository](https://github.com/citusdata/pg_cron)
|
||||
@@ -4,9 +4,13 @@ title: 'PGAudit: Postgres Auditing'
|
||||
description: 'Session and object auditing via PostgreSQL standard logging'
|
||||
---
|
||||
|
||||
[PGAudit](https://www.pgaudit.org) is a PostgreSQL extension for logging session and object auditing over the standard PostgreSQL logging utility.
|
||||
[PGAudit](https://www.pgaudit.org) extends Postgres's built-in logging abilities. It can be used to selectively track activities within your database.
|
||||
|
||||
PGAudit grants fine grain control over which statements and objects are emitted to logs.
|
||||
This helps you with:
|
||||
|
||||
- **Compliance**: Meeting audit requirements for regulations
|
||||
- **Security**: Detecting suspicious database activity
|
||||
- **Troubleshooting**: Identifying and fixing database issues
|
||||
|
||||
## Enable the extension
|
||||
|
||||
@@ -26,7 +30,6 @@ PGAudit grants fine grain control over which statements and objects are emitted
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
-- Enable the "pgaudit" extension
|
||||
create extension pgaudit;
|
||||
@@ -35,74 +38,358 @@ create extension pgaudit;
|
||||
drop extension if exists pgaudit;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Settings
|
||||
## Configure the extension
|
||||
|
||||
The `pgaudit.log` setting controls which statements to log. Available values include:
|
||||
PGAudit can be configured with different levels of precision.
|
||||
|
||||
- `read`: `SELECT` and `COPY` when the source is a relation or a query.
|
||||
- `write`: `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, and `COPY` when the destination is a relation.
|
||||
- `function`: Function calls and `DO` blocks.
|
||||
- `role`: Statements related to roles and privileges: `GRANT`, `REVOKE`, `CREATE/ALTER/DROP ROLE`.
|
||||
- `ddl`: All `DDL` that is not included in the `ROLE` class.
|
||||
- `misc`: Miscellaneous commands, for example, `DISCARD`, `FETCH`, `CHECKPOINT`, `VACUUM`, `SET`.
|
||||
- `misc_set`: Miscellaneous `SET` commands, for example, `SET ROLE`.
|
||||
**PGAudit logging precision:**
|
||||
|
||||
- **all**: Include all of the above.
|
||||
- **[Session](#session-logging):** Logs activity within a connection, such as a [psql](https://supabase.com/docs/guides/database/connecting-to-postgres#connecting-with-psql) connection.
|
||||
- **[User](#user-logging):** Logs activity by a particular database user (for example, anon or postgres).
|
||||
- **[Global](#global-logging):** Logs activity across the entire database.
|
||||
- **[Object](#object-logging):** Logs events related to specific database objects (for example, the auth.users table).
|
||||
|
||||
For a full list of available settings see [settings docs](https://github.com/pgaudit/pgaudit/blob/master/README.md#settings). Be aware that the `all` setting will generate a very large volume of logs.
|
||||
Although Session, User, and Global modes differ in their precision, they're all considered variants of **Session Mode** and are configured with the same input categories.
|
||||
|
||||
### Session mode categories
|
||||
|
||||
These modes can monitor predefined categories of database operations:
|
||||
|
||||
| Category | What it Logs | Description |
|
||||
| -------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| read | Data retrieval (SELECT, COPY) | Tracks what data is being accessed. |
|
||||
| write | Data modification (INSERT, DELETE, UPDATE, TRUNCATE, COPY) | Tracks changes made to your database. |
|
||||
| function | FUNCTION, PROCEDURE, and DO/END block executions | Tracks routine/function executions |
|
||||
| role | User management actions (CREATE, DROP, ALTER on users and privileges) | Tracks changes to user permissions and access. |
|
||||
| ddl | Schema changes (CREATE, DROP, ALTER statements) | Monitors modifications to your database structure (tables, indexes, etc.). |
|
||||
| misc | Less common commands (FETCH, CHECKPOINT) | Captures obscure actions for deeper analysis if needed. |
|
||||
| all | Everything above | Comprehensive logging for complete audit trails. |
|
||||
|
||||
Below is a limited example of how to assign PGAudit to monitor specific categories.
|
||||
|
||||
```sql
|
||||
-- log all CREATE, ALTER, and DROP events
|
||||
... pgaudit.log = 'ddl';
|
||||
|
||||
-- log all CREATE, ALTER, DROP, and SELECT events
|
||||
... pgaudit.log = 'read, ddl';
|
||||
|
||||
-- log nothing
|
||||
... pgaudit.log = 'none';
|
||||
```
|
||||
|
||||
### Session logging
|
||||
|
||||
When you are connecting in a session environment, such as a [psql](https://supabase.com/docs/guides/database/connecting-to-postgres#connecting-with-psql) connection, you can configure PGAudit to record events initiated within the session.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
We don't currently support configuring `pgaudit.log_parameter` because it may log secrets in encrypted columns if you are using [`pgsodium`](/docs/guides/database/extensions/pgsodium) or [Vault](/docs/guides/database/vault).
|
||||
|
||||
You can upvote this [feature request](https://github.com/orgs/supabase/discussions/20183) if you'd like this restriction lifted.
|
||||
The [Dashboard](https://supabase.com/dashboard/project/_) is a transactional environment and won't sustain a session.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Example
|
||||
Inside a session, by default, PGAudit will log nothing:
|
||||
|
||||
Given a `pgaudit` setting
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
set pgaudit.log = 'read, ddl';
|
||||
-- returns 'none'
|
||||
show pgaudit.log;
|
||||
```
|
||||
|
||||
The following create table, insert and select statements
|
||||
In the session, you can `set` the `pgaudit.log` variable to record events:
|
||||
|
||||
```sql
|
||||
-- log CREATE, ALTER, and DROP events
|
||||
set pgaudit.log = 'ddl';
|
||||
|
||||
-- log all CREATE, ALTER, DROP, and SELECT events
|
||||
set pgaudit.log = 'read, ddl';
|
||||
|
||||
-- log nothing
|
||||
set pgaudit.log = 'none';
|
||||
```
|
||||
|
||||
### User logging
|
||||
|
||||
There are some cases where you may want to monitor a database user's actions. For instance, let's say you connected your database to [Zapier](https://supabase.com/partners/integrations/zapier) and created a custom role for it to use:
|
||||
|
||||
```sql
|
||||
create user "zapier" with password '<new password>';
|
||||
```
|
||||
|
||||
You may want to log all actions initiated by `zapier`, which can be done with the following command:
|
||||
|
||||
```sql
|
||||
alter role "zapier" set pgaudit.log to 'all';
|
||||
```
|
||||
|
||||
To remove the settings, execute the following code:
|
||||
|
||||
```sql
|
||||
-- disables role's log
|
||||
alter role "zapier" set pgaudit.log to 'none';
|
||||
|
||||
-- check to make sure the changes are finalized:
|
||||
select
|
||||
rolname,
|
||||
rolconfig
|
||||
from pg_roles
|
||||
where rolname = 'zapier';
|
||||
-- should return a rolconfig path with "pgaudit.log=none" present
|
||||
```
|
||||
|
||||
### Global logging
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Use global logging cautiously. It can generate many logs and make it difficult to find important events. Consider limiting the scope of what is logged by using session, user, or object logging where possible.
|
||||
|
||||
</Admonition>
|
||||
|
||||
The below SQL configures PGAudit to record all events associated with the "postgres" role. Since it has extensive privileges, this effectively monitors all database activity.
|
||||
|
||||
```sql
|
||||
alter role "postgres" set pgaudit.log to 'all';
|
||||
```
|
||||
|
||||
To check if the postgres role is auditing, execute the following command:
|
||||
|
||||
```sql
|
||||
select
|
||||
rolname,
|
||||
rolconfig
|
||||
from pg_roles
|
||||
where rolname = 'postgres';
|
||||
-- should return a rolconfig path with "pgaudit.log=all" present
|
||||
```
|
||||
|
||||
To remove the settings, execute the following code:
|
||||
|
||||
```sql
|
||||
alter role "postgres" set pgaudit.log to 'none';
|
||||
```
|
||||
|
||||
### Object logging
|
||||
|
||||
To fine-tune what object events PGAudit will record, you must create a custom database role with limited permissions:
|
||||
|
||||
```sql
|
||||
create role "some_audit_role" noinherit;
|
||||
```
|
||||
|
||||
No other Postgres user can assume or login via this role. It solely exists to securely define what PGAudit will record.
|
||||
|
||||
Once the role is created, you can direct PGAudit to log by assigning it to the `pgaudit.role` variable:
|
||||
|
||||
```sql
|
||||
alter role "postgres" set pgaudit.role to 'some_audit_role';
|
||||
```
|
||||
|
||||
You can then assign the role to monitor only approved object events, such as `select` statements that include a specific table:
|
||||
|
||||
```sql
|
||||
grant select on random_table to "some_audit_role";
|
||||
```
|
||||
|
||||
With this privilege granted, PGAudit will record all select statements that reference the `random_table`, regardless of _who_ or _what_ actually initiated the event. All assignable privileges can be viewed in the [Postgres documentation](https://www.postgresql.org/docs/current/ddl-priv.html).
|
||||
|
||||
If you would no longer like to use object logging, you will need to unassign the `pgaudit.role` variable:
|
||||
|
||||
```sql
|
||||
-- change pgaudit.role to no longer reference some_audit_role
|
||||
alter role "postgres" set pgaudit.role to '';
|
||||
|
||||
-- view if pgaudit.role changed with the following command:
|
||||
select
|
||||
rolname,
|
||||
rolconfig
|
||||
from pg_roles
|
||||
where rolname = 'postgres';
|
||||
-- should return a rolconfig path with "pgaudit.role="
|
||||
```
|
||||
|
||||
## Interpreting Audit Logs
|
||||
|
||||
PGAudit was designed for storing logs as CSV files with the following headers:
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Referenced from the [PGAudit official docs](https://github.com/pgaudit/pgaudit/blob/master/README.md#format)
|
||||
|
||||
</Admonition>
|
||||
|
||||
| header | Description |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| AUDIT_TYPE | SESSION or OBJECT |
|
||||
| STATEMENT_ID | Unique statement ID for this session. Sequential even if some statements are not logged. |
|
||||
| SUBSTATEMENT_ID | Sequential ID for each sub-statement within the main statement. Continuous even if some are not logged. |
|
||||
| CLASS | ..., READ, ROLE (see pgaudit.log). |
|
||||
| COMMAND | ..., ALTER TABLE, SELECT. |
|
||||
| OBJECT_TYPE | TABLE, INDEX, VIEW, etc. Available for SELECT, DML, and most DDL statements. |
|
||||
| OBJECT_NAME | The fully qualified object name (for example, public.account). Available for SELECT, DML, and most DDL. |
|
||||
| STATEMENT | Statement executed on the backend. |
|
||||
| PARAMETER | If pgaudit.log_parameter is set, this field contains the statement parameters as quoted CSV, or \<none\>. Otherwise, it's \<not logged\>. |
|
||||
|
||||
A log made from the following create statement:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
create table account (
|
||||
id int primary key,
|
||||
name text,
|
||||
description text
|
||||
);
|
||||
|
||||
insert into account (id, name, description)
|
||||
values (1, 'Foo Barsworth', 'Customer account');
|
||||
|
||||
select * from account;
|
||||
```
|
||||
|
||||
Results in the log output
|
||||
Generates the following log in the [Dashboard's Postgres Logs](https://supabase.com/dashboard/project/_/logs/postgres-logs):
|
||||
|
||||
```text
|
||||
AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account(
|
||||
```
|
||||
AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account(
|
||||
id int,
|
||||
name text,
|
||||
description text
|
||||
);,<not logged>
|
||||
AUDIT: SESSION,2,1,READ,SELECT,,,select * from account,,<not logged>
|
||||
); <not logged>
|
||||
```
|
||||
|
||||
Note that the insert statement is not logged because we did not include the `write` option for `pgaudit.log`.
|
||||
## Finding and filtering audit logs
|
||||
|
||||
Logs generated by PGAudit can be found in [Postgres Logs](https://supabase.com/dashboard/project/_/logs/postgres-logs?s=AUDIT). To find a specific log, you can use the log explorer. Below is a basic example to extract logs referencing `CREATE TABLE` events
|
||||
|
||||
```sql
|
||||
select
|
||||
cast(t.timestamp as datetime) as timestamp,
|
||||
event_message
|
||||
from
|
||||
postgres_logs as t
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.parsed) as p
|
||||
where event_message like 'AUDIT%CREATE TABLE%'
|
||||
order by timestamp desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
## Practical examples
|
||||
|
||||
### Monitoring API events
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
API requests are already recorded in the [API Edge Network](https://supabase.com/dashboard/project/_/logs/edge-logs) logs.
|
||||
|
||||
</Admonition>
|
||||
|
||||
To monitor all writes initiated by the Postgrest API roles:
|
||||
|
||||
```sql
|
||||
alter role "authenticator" set pgaudit.log to 'write';
|
||||
|
||||
-- the above is the practical equivalent to:
|
||||
-- alter role "anon" set pgaudit.log TO 'write';
|
||||
-- alter role "authenticated" set pgaudit.log TO 'write';
|
||||
-- alter role "service_role" set pgaudit.log TO 'write';
|
||||
```
|
||||
|
||||
### Monitoring the `auth.users` table
|
||||
|
||||
In the worst case scenario, where a privileged roles' password is exposed, you can use PGAudit to monitor if the `auth.users` table was targeted. It should be stated that API requests are already monitored in the [API Edge Network](https://supabase.com/dashboard/project/_/logs/edge-logs) and this is more about providing greater clarity about what is happening at the database level.
|
||||
|
||||
Logging `auth.user` should be done in Object Mode and requires a custom role:
|
||||
|
||||
```sql
|
||||
-- create logging role
|
||||
create role "auth_auditor" noinherit;
|
||||
|
||||
-- give role permission to observe relevant table events
|
||||
grant select on auth.users to "auth_auditor";
|
||||
grant delete on auth.users to "auth_auditor";
|
||||
|
||||
-- assign auth_auditor to pgaudit.role
|
||||
alter role "postgres" set pgaudit.role to 'auth_auditor';
|
||||
```
|
||||
|
||||
With the above code, any query involving reading or deleting from the auth.users table will be logged.
|
||||
|
||||
## Best practices
|
||||
|
||||
### Disabling excess logging
|
||||
|
||||
PGAudit, if not configured mindfully, can log all database events, including background tasks. This can generate an undesirably large amount of logs in a few hours.
|
||||
|
||||
The first step to solve this problem is to identify which database users PGAudit is observing:
|
||||
|
||||
```sql
|
||||
-- find all users monitored by pgaudit
|
||||
select
|
||||
rolname,
|
||||
rolconfig
|
||||
from pg_roles
|
||||
where
|
||||
exists (
|
||||
select
|
||||
1
|
||||
from UNNEST(rolconfig) as c
|
||||
where c like '%pgaudit.role%' or c like '%pgaudit.log%'
|
||||
);
|
||||
```
|
||||
|
||||
To prevent PGAudit from monitoring the problematic roles, you'll want to change their `pgaudit.log` values to `none` and `pgaudit.role` values to `empty quotes ''`
|
||||
|
||||
```sql
|
||||
-- Use to disable object level logging
|
||||
alter role "<role name>" set pgaudit.role to '';
|
||||
|
||||
-- Use to disable global and user level logging
|
||||
alter role "<role name>" set pgaudit.log to 'none';
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
#### Can I use PGAudit to debug database functions?
|
||||
|
||||
Technically yes, but it is not the best approach. It is better to check out our [function debugging guide](https://supabase.com/docs/guides/database/functions#general-logging) instead.
|
||||
|
||||
#### How can I download database logs?
|
||||
|
||||
In the [Logs Dashboard](https://supabase.com/dashboard/project/_/logs/postgres-logs) you can download logs as CSVs.
|
||||
|
||||
#### Can I log observed table rows?
|
||||
|
||||
By default, PGAudit records queries, but not the returned rows. You can modify this behavior with the `pgaudit.log_rows` variable:
|
||||
|
||||
```sql
|
||||
--enable
|
||||
alter role "postgres" set pgaudit.log_rows to 'on';
|
||||
|
||||
-- disable
|
||||
alter role "postgres" set pgaudit.log_rows to 'off';
|
||||
```
|
||||
|
||||
You should not do this unless you are _absolutely_ certain it is necessary for your use case. It can expose sensitive values to your logs that ideally should not be preserved. Furthermore, if done in excess, it can noticeably reduce database performance.
|
||||
|
||||
#### Can I log function parameters?
|
||||
|
||||
We don't currently support configuring `pgaudit.log_parameter` because it may log secrets in encrypted columns if you are using [pgsodium](https://supabase.com/docs/guides/database/extensions/pgsodium) or[Vault](https://supabase.com/docs/guides/database/vault).
|
||||
|
||||
You can upvote this [feature request](https://github.com/orgs/supabase/discussions/20183) with your use-case if you'd like this restriction lifted.
|
||||
|
||||
#### Does PGAudit support system wide configurations?
|
||||
|
||||
PGAudit allows settings to be applied to 3 different database scopes:
|
||||
|
||||
| Scope | Description | Configuration File/Command |
|
||||
| -------- | ------------------ | -------------------------- |
|
||||
| System | Entire server | ALTER SYSTEM commands |
|
||||
| Database | Specific database | ALTER DATABASE commands |
|
||||
| Role | Specific user/role | ALTER ROLE commands |
|
||||
|
||||
Supabase limits full privileges for file system and database variables, meaning PGAudit modifications can only occur at the role level. Assigning PGAudit to the 'postgres' role grants it nearly complete visibility into the database, making role-level adjustments a practical alternative to configuring at the database or system level.
|
||||
|
||||
PGAudit's [official documentation](https://www.pgaudit.org) focuses on system and database level configs, but its docs officially supports role level configs, too.
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [`PGAudit` documentation](https://www.pgaudit.org)
|
||||
- [Official `PGAudit` documentation](https://www.pgaudit.org)
|
||||
- [Database Function Logging](https://supabase.com/docs/guides/database/functions#general-logging)
|
||||
- [Supabase Logging](https://supabase.com/docs/guides/platform/logs)
|
||||
- [Self-Hosting Logs](https://supabase.com/docs/reference/self-hosting-analytics/introduction)
|
||||
Loaded 100 of 1403 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user