Merge branch 'master' into remove-JSON-trailing-commas

This commit is contained in:
Faizan Rasool authored and GitHub committed 2024-02-04 20:48:49 -05:00
commit 1783d322e9
385 files changed
+4049 -2288

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
name: Autofix Linting Checks
on:
pull_request:
types:
- labeled
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
autofix:
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ github.event_name == 'pull_request' && (github.event.label.name == 'autofix') }}
steps:
- name: Calculate number of commits
run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}"
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
sparse-checkout: apps
fetch-depth: ${{ env.PR_FETCH_DEPTH }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
- name: Install required dependencies
run: |
rm package.json
rm package-lock.json
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
- name: Run Prettier in fix mode
run: |
npx prettier --write 'apps/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
- name: Commit changes and push to existing branch
run: |
git checkout -- package.json
git checkout -- package-lock.json
git config --global user.name 'github-tidy-bot'
git config --global user.email 'github-tidy-bot@supabase.com'
if [[ `git status --porcelain` ]]; then
echo "[bot] Changes detected, committing."
echo "[bot] Running in non-squash mode."
git commit -am "ci: Autofix updates from GitHub workflow"
git push
else
echo "[bot] No changes detected, nothing to commit."
fi
+3 -3
View File
@@ -27,7 +27,7 @@ jobs:
run: |
rm package.json
rm package-lock.json
npm i prettier@2 prettier-plugin-sql-cst
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
- name: Run prettier
run: |-
npx prettier -c 'apps/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
@@ -50,7 +50,7 @@ jobs:
run: |
rm package.json
rm package-lock.json
npm i prettier@2 prettier-plugin-sql-cst
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
- name: Run prettier
run: |-
npx prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
@@ -68,7 +68,7 @@ jobs:
run: |
rm package.json
rm package-lock.json
npm i prettier@2 prettier-plugin-sql-cst
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
- name: Run prettier
run: |-
# Check mdx files which contain sql code blocks
@@ -138,8 +138,8 @@ const UserChat = ({ message, isLatest, times, run }: UserChatProps) => {
{replyDuration !== undefined
? ` with ${replyDuration}s response`
: isLoading
? ', generating response...'
: ''}
? ', generating response...'
: ''}
</p>
)}
</div>
+18 -9
View File
@@ -8,56 +8,64 @@
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-Book.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-Book.woff2) format('woff2'),
url(/fonts/custom/CustomFont-Book.woff) format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-BookItalic.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-BookItalic.woff2) format('woff2'),
url(/fonts/custom/CustomFont-BookItalic.woff) format('woff');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-Medium.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-Medium.woff2) format('woff2'),
url(/fonts/custom/CustomFont-Medium.woff) format('woff');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-MediumItalic.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-MediumItalic.woff2) format('woff2'),
url(/fonts/custom/CustomFont-MediumItalic.woff) format('woff');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-Bold.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-Bold.woff2) format('woff2'),
url(/fonts/custom/CustomFont-Bold.woff) format('woff');
font-weight: 700;
font-style: 600;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-BoldItalic.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-BoldItalic.woff2) format('woff2'),
url(/fonts/custom/CustomFont-BoldItalic.woff) format('woff');
font-style: 600;
font-style: italic;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-Black.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-Black.woff2) format('woff2'),
url(/fonts/custom/CustomFont-Black.woff) format('woff');
font-weight: 800;
font-style: normal;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-BlackItalic.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-BlackItalic.woff2) format('woff2'),
url(/fonts/custom/CustomFont-BlackItalic.woff) format('woff');
font-weight: 800;
font-style: italic;
@@ -66,7 +74,8 @@
@font-face {
font-family: 'source code pro';
src: url('/fonts/source-code-pro/SourceCodePro-Regular.eot');
src: url('/fonts/source-code-pro/SourceCodePro-Regular.woff2') format('woff2'),
src:
url('/fonts/source-code-pro/SourceCodePro-Regular.woff2') format('woff2'),
url('/fonts/source-code-pro/SourceCodePro-Regular.woff') format('woff'),
url('/fonts/source-code-pro/SourceCodePro-Regular.ttf') format('truetype'),
url('/fonts/source-code-pro/SourceCodePro-Regular.svg#SourceCodePro-Regular') format('svg');
+1 -1
View File
@@ -17,7 +17,7 @@
"common": "*",
"config": "*",
"dayjs": "^1.11.10",
"eslint-config-next": "^14.0.3",
"eslint-config-next": "^14.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.292.0",
"next": "^14.0.3",
+5 -5
View File
@@ -15,15 +15,15 @@
"incremental": true,
"plugins": [
{
"name": "next"
}
"name": "next",
},
],
"paths": {
"@/*": ["./*"],
// handle ui package paths
"@ui/*": ["./../../packages/ui/src/*"]
}
"@ui/*": ["./../../packages/ui/src/*"],
},
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
}
+3 -3
View File
@@ -46,7 +46,7 @@ export interface Database {
columns: ['user_id']
referencedRelation: 'users'
referencedColumns: ['id']
}
},
]
}
responses_ai: {
@@ -92,7 +92,7 @@ export interface Database {
columns: ['user_id']
referencedRelation: 'users'
referencedColumns: ['id']
}
},
]
}
threads: {
@@ -132,7 +132,7 @@ export interface Database {
columns: ['user_id']
referencedRelation: 'users'
referencedColumns: ['id']
}
},
]
}
}
@@ -64,8 +64,8 @@ export function ComboBox<Opt extends ComboBoxOption>({
{isLoading
? 'Loading...'
: options.length === 0
? `No ${name} found`
: selectedOptionDisplayName ?? `Select a ${name}...`}
? `No ${name} found`
: selectedOptionDisplayName ?? `Select a ${name}...`}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
@@ -98,14 +98,14 @@ function OrgProjectSelector() {
const stateSummary: ProjectOrgDataState = isUserLoading
? 'userLoading'
: !isLoggedIn
? 'loggedOut'
: anyIsPending
? 'loggedIn.dataPending'
: anyIsError
? 'loggedIn.dataError'
: projects?.length === 0
? 'loggedIn.dataSuccess.hasNoData'
: 'loggedIn.dataSuccess.hasData'
? 'loggedOut'
: anyIsPending
? 'loggedIn.dataPending'
: anyIsError
? 'loggedIn.dataError'
: projects?.length === 0
? 'loggedIn.dataSuccess.hasNoData'
: 'loggedIn.dataSuccess.hasData'
const formattedData: ComboBoxOption[] = useMemo(
() =>
@@ -194,16 +194,16 @@ function BranchSelector() {
const stateSummary: BranchesDataState = userLoading
? 'userLoading'
: !isLoggedIn
? 'loggedOut'
: !hasBranches
? 'loggedIn.noBranches'
: isPending
? 'loggedIn.branches.dataPending'
: isError
? 'loggedIn.branches.dataError'
: data.length === 0
? 'loggedIn.branches.dataSuccess.noData'
: 'loggedIn.branches.dataSuccess.hasData'
? 'loggedOut'
: !hasBranches
? 'loggedIn.noBranches'
: isPending
? 'loggedIn.branches.dataPending'
: isError
? 'loggedIn.branches.dataError'
: data.length === 0
? 'loggedIn.branches.dataSuccess.noData'
: 'loggedIn.branches.dataSuccess.hasData'
const formattedData: ComboBoxOption[] =
stateSummary !== 'loggedIn.branches.dataSuccess.hasData'
@@ -282,14 +282,14 @@ function VariableView({ variable, className }: { variable: Variable; className?:
const stateSummary: VariableDataState = isUserLoading
? 'userLoading'
: !isLoggedIn
? 'loggedOut'
: !ref
? 'loggedIn.noSelectedProject'
: isPending
? 'loggedIn.selectedProject.dataPending'
: isError
? 'loggedIn.selectedProject.dataError'
: 'loggedIn.selectedProject.dataSuccess'
? 'loggedOut'
: !ref
? 'loggedIn.noSelectedProject'
: isPending
? 'loggedIn.selectedProject.dataPending'
: isError
? 'loggedIn.selectedProject.dataError'
: 'loggedIn.selectedProject.dataSuccess'
let variableValue: string = null
if (stateSummary === 'loggedIn.selectedProject.dataSuccess') {
@@ -319,8 +319,8 @@ function VariableView({ variable, className }: { variable: Variable; className?:
stateSummary === 'loggedIn.selectedProject.dataPending'
? 'Loading...'
: stateSummary === 'loggedIn.selectedProject.dataSuccess'
? variableValue
: `YOUR ${prettyFormatVariable[variable].toUpperCase()}`
? variableValue
: `YOUR ${prettyFormatVariable[variable].toUpperCase()}`
}
/>
<CopyToClipboard text={variableValue ?? ''}>
@@ -158,8 +158,8 @@ export default function RealtimeLimitsEstimater({}) {
{computeAddOn === 'micro'
? 'Micro'
: computeAddOn === 'small'
? 'Small to medium'
: 'Large to 16XL'}
? 'Small to medium'
: 'Large to 16XL'}
</h4>
<table className="table-auto">
<thead>
@@ -126,14 +126,14 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
const codeBlockLang = example?.code?.startsWith('```js')
? 'js'
: example?.code?.startsWith('```ts')
? 'ts'
: example?.code?.startsWith('```dart')
? 'dart'
: example?.code?.startsWith('```c#')
? 'csharp'
: example?.code?.startsWith('```kotlin')
? 'kotlin'
: 'js'
? 'ts'
: example?.code?.startsWith('```dart')
? 'dart'
: example?.code?.startsWith('```c#')
? 'csharp'
: example?.code?.startsWith('```kotlin')
? 'kotlin'
: 'js'
// `
// import { createClient } from '@supabase/supabase-js'
@@ -185,7 +185,7 @@ Supabase.js v2 release notes.
<RefSubLayout.Details>
We're removing the `signIn()` method in favor of more explicit function signatures:
`signInWithPassword()`, `signInWithOtp()`, and `signInWithOtp()`.
`signInWithPassword()`, `signInWithOtp()`, and `signInWithOAuth()`.
</RefSubLayout.Details>
<RefSubLayout.Examples>
+4 -4
View File
@@ -259,10 +259,10 @@ function extractParamTypeAsString(paramDefinition) {
x.value
? `<code>${x.value}</code>`
: x.name
? `<code>${x.name}</code>`
: x.type
? `<code>${x.type}</code>`
: ''
? `<code>${x.name}</code>`
: x.type
? `<code>${x.type}</code>`
: ''
)
.join(' | ')
}
+3 -3
View File
@@ -5,9 +5,9 @@
"noImplicitAny": false,
"baseUrl": ".",
"paths": {
"~/*": ["./*"]
}
"~/*": ["./*"],
},
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
}
+1 -1
View File
@@ -53,7 +53,7 @@ async function generate() {
</urlset>
`
const formatted = prettier.format(sitemap, {
const formatted = await prettier.format(sitemap, {
...prettierConfig,
parser: 'html',
})
+3 -3
View File
@@ -7,9 +7,9 @@
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"~/*": ["./*"]
}
"~/*": ["./*"],
},
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
}
+5 -6
View File
@@ -201,8 +201,8 @@ const MobileHeader = memo(function MobileHeader() {
{mobileMenuOpen
? 'Close'
: menuLevel
? levelsData[menuLevel].name
: levelsData['home'].name}
? levelsData[menuLevel].name
: levelsData['home'].name}
</span>
</div>
)
@@ -267,7 +267,7 @@ const Container = memo(function Container(props: PropsWithChildren) {
id="docs-content-container"
className={[
// 'overflow-x-auto',
'w-full h-screen transition-all ease-out',
'w-full transition-all ease-out',
// 'absolute lg:relative',
mobileMenuOpen ? 'ml-[75%] sm:ml-[50%] md:ml-[33%] overflow-hidden' : 'overflow-auto',
// desktop override any margin styles
@@ -294,7 +294,6 @@ const NavContainer = memo(function NavContainer() {
'lg:left-0',
'transition-all',
'top-0',
'h-screen',
'flex flex-col ml-0',
].join(' ')}
>
@@ -352,8 +351,8 @@ const SiteLayout = ({ children }: PropsWithChildren<{}>) => {
<Head>
<title>Supabase Docs</title>
</Head>
<main>
<div className="flex flex-row h-screen">
<main className="grow overflow-hidden">
<div className="flex flex-row h-full">
<NavContainer />
<Container>
<div className={['lg:sticky top-0 z-10 overflow-hidden'].join(' ')}>
+1 -1
View File
@@ -39,7 +39,7 @@
"gray-matter": "^4.0.3",
"hast-util-has-property": "^2.0.1",
"isbot": "^3.6.5",
"jsrsasign": "^10.5.26",
"jsrsasign": "^11.0.0",
"lodash": "^4.17.21",
"markdown-toc": "^1.2.0",
"mdast": "^3.0.0",
+7 -5
View File
@@ -191,16 +191,18 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<>
<QueryClientProvider client={queryClient}>
<Favicons />
<IPv4DeprecationBanner />
<AuthContainer>
<SignOutHandler>
<ThemeProvider defaultTheme="system" enableSystem disableTransitionOnChange>
<CommandMenuProvider site="docs">
<TabsProvider>
<SiteLayout>
<PortalToast />
<Component {...pageProps} />
</SiteLayout>
<div className="h-screen flex flex-col">
<IPv4DeprecationBanner />
<SiteLayout>
<PortalToast />
<Component {...pageProps} />
</SiteLayout>
</div>
</TabsProvider>
</CommandMenuProvider>
</ThemeProvider>
@@ -37,7 +37,7 @@ There are 3 steps to build similarity search inside your documentation:
### Prepare your database
To prepare, create a [new Supabase project](https://database.new) and store the database and API credentials, which you can find in the project [settings](https://supabase.com/dashboard/_/settings).
To prepare, create a [new Supabase project](https://database.new) and store the database and API credentials, which you can find in the project [settings](https://supabase.com/dashboard/project/_/settings).
Now we can use the [Headless Vector Search](https://github.com/supabase/headless-vector-search#set-up) instructions to set up the database:
+1 -1
View File
@@ -39,7 +39,7 @@ pip install vecs
## Connect to your database
Find the Postgres pooler connection string for your Supabase project in the [database settings](https://supabase.com/dashboard/_/settings/database) of the dashboard. Copy the "URI" format, which should look something like `postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres`
Find the Postgres pooler connection string for your Supabase project in the [database settings](https://supabase.com/dashboard/project/_/settings/database) of the dashboard. Copy the "URI" format, which should look something like `postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres`
Create a new code block below the install block (`ctrl+m b`) and add the following code using the Postgres URI you copied above:
@@ -1,3 +1,4 @@
import { Admonition } from 'ui'
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
@@ -46,7 +47,19 @@ DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
vx = vecs.create_client(DB_CONNECTION)
```
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project.
<Admonition type='note'>
SQLAlchemy requires the connection string to start with `postgresql://` (instead of `postgres://`). Don't forget to rename this after copying the string from the dashboard.
</Admonition>
<Admonition type='note'>
You must use the "connection pooling" string (domain ending in `*.pooler.supabase.com`) with Google Colab since Colab does not support IPv6.
</Admonition>
## Stepping through the notebook
@@ -11,6 +11,12 @@ As described in [Structured & Unstructured Embeddings](/docs/guides/ai/structure
For data science or ephemeral workloads, the [Supabase Vecs](https://supabase.github.io/vecs/) client gets you started quickly. All you need is a connection string and vecs handles setting up your database to store and query vectors with associated metadata.
<Admonition type="tip">
You can get your connection string from the [**Database Settings**](https://supabase.com/dashboard/project/_/settings/database) page in your dashboard. Make sure to check **Use connection pooling**, then copy the URI. Also, change the URI scheme from `postgres` to `postgresql`. `vecs` uses SQLAlchemy under the hood, which only supports `postgresql` as a dialect.
</Admonition>
For production python applications with version controlled migrations, we recommend adding first class vector support to your toolchain by [registering the vector type with your ORM](https://github.com/pgvector/pgvector-python). pgvector provides bindings for the most commonly used SQL drivers/libraries including Django, SQLAlchemy, SQLModel, psycopg, asyncpg and Peewee.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -1,5 +1,5 @@
import { Admonition } from 'ui'
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-vecs-python-client',
title: 'Face similarity search',
@@ -43,7 +43,19 @@ DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
vx = vecs.create_client(DB_CONNECTION)
```
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project.
<Admonition type='note'>
SQLAlchemy requires the connection string to start with `postgresql://` (instead of `postgres://`). Don't forget to rename this after copying the string from the dashboard.
</Admonition>
<Admonition type='note'>
You must use the "connection pooling" string (domain ending in `*.pooler.supabase.com`) with Google Colab since Colab does not support IPv6.
</Admonition>
## Stepping through the notebook
@@ -1,3 +1,4 @@
import { Admonition } from 'ui'
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
@@ -43,7 +44,19 @@ DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
vx = vecs.create_client(DB_CONNECTION)
```
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project.
<Admonition type='note'>
SQLAlchemy requires the connection string to start with `postgresql://` (instead of `postgres://`). Don't forget to rename this after copying the string from the dashboard.
</Admonition>
<Admonition type='note'>
You must use the "connection pooling" string (domain ending in `*.pooler.supabase.com`) with Google Colab since Colab does not support IPv6.
</Admonition>
## Stepping through the notebook
@@ -1,5 +1,6 @@
import Layout from '~/layouts/DefaultGuideLayout'
import { Admonition } from 'ui'
import HuggingFaceDeployment from '~/components/MDX/ai/quickstart_hf_deployment.mdx'
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-vecs-python-client',
@@ -44,7 +45,19 @@ DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
vx = vecs.create_client(DB_CONNECTION)
```
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project.
<Admonition type='note'>
SQLAlchemy requires the connection string to start with `postgresql://` (instead of `postgres://`). Don't forget to rename this after copying the string from the dashboard.
</Admonition>
<Admonition type='note'>
You must use the "connection pooling" string (domain ending in `*.pooler.supabase.com`) with Google Colab since Colab does not support IPv6.
</Admonition>
## Stepping through the notebook
@@ -1,250 +0,0 @@
import Layout from '~/layouts/DefaultGuideLayout'
import { Accordion } from 'ui'
export const meta = {
id: 'auth-password-reset',
title: 'Password Reset',
description: 'How to reset password with Supabase Auth',
}
- Send email for password reset using [`.resetPasswordForEmail`](/docs/reference/javascript/auth-resetpasswordforemail) while providing a `redirectTo` parameter
- Email link will work as a magic link and log the user in then take them to the url specified in the `redirectTo` parameter
- Create form to update the password and call the [`.updateUser`](/docs/reference/javascript/auth-updateuser) method with the new password
## Single Page Application (SPA)
### Sending password reset email
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
Supabase provides a convenient method [`.resetPasswordForEmail`](/docs/reference/javascript/auth-resetpasswordforemail) to reset a user password. This method takes a parameter of `redirectTo` which we will use to pass 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) or it won't redirect the user.
```js
await supabase.auth.resetPasswordForEmail('hello@example.com', {
redirectTo: 'http://example.com/account/update-password',
})
```
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
Supabase provides a convenient method [`.resetPasswordForEmail`](/docs/reference/kotlin/auth-resetpasswordforemail) to reset a user password. This method takes a parameter of `redirectUrl` which we will use to pass 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) or it won't redirect the user.
```kotlin
supabase.auth.resetPasswordForEmail(
email = "hello@example.com",
redirectUrl = "http://example.com/account/update-password"
)
```
If you are on one of the Kotlin targets that have built-in support for redirect url handling like Android, you may want to checkout [OAuth and OTP link verification](https://supabase.com/docs/reference/kotlin/initializing).
</TabPanel>
</Tabs>
### Email link
The email link you receive will work like a magic link. This way when you click the link you will be logged into the website. Since we passed a redirect URL to the [`.resetPasswordForEmail`](https://supabase.com/docs/reference/javascript/auth-resetpasswordforemail) method the user should be sent to the update password page.
### Update password
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
To update the password we call the [`.updateUser`](/docs/reference/javascript/auth-updateuser) method and pass along the new password to this method.
```js
await supabase.auth.updateUser({ password: new_password })
```
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
To update the password we call the [`.modifyUser`](/docs/reference/kotlin/auth-updateuser) method and pass along the new password to this method.
```kotlin
supabase.auth.modifyUser {
password = "new_password"
}
```
</TabPanel>
</Tabs>
## Server-Side Rendering (SSR)
### Sending password reset email
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
Supabase provides a convenient method [`.resetPasswordForEmail`](/docs/reference/javascript/auth-resetpasswordforemail) to reset a user password. This method takes a parameter of `redirectTo` which we will use to pass an absolute URL to the callback page along with a query parameter 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) or it won't redirect the user.
```js
await supabase.auth.resetPasswordForEmail('hello@example.com', {
redirectTo: 'http://example.com/api/auth/callback?next=/account/update-password',
})
```
<Admonition type="note">
We are using `next` as our query parameter, but this can name whatever you like.
</Admonition>
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
Supabase provides a convenient method [`.resetPasswordForEmail`](/docs/reference/kotlin/auth-resetpasswordforemail) to reset a user password. This method takes a parameter of `redirectUrl` which we will use to pass 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) or it won't redirect the user.
```kotlin
supabase.auth.resetPasswordForEmail(
email = "hello@example.com",
redirectUrl = "http://example.com/api/auth/callback?next=/account/update-password"
)
```
If you are on one of the Kotlin targets that have built-in support for redirect url handling like Android, you may want to checkout [OAuth and OTP link verification](https://supabase.com/docs/reference/kotlin/initializing).
</TabPanel>
</Tabs>
### Email link
The email link you receive will behave like a magic link. When the link is clicked you will be sent to the `redirectTo` URL you specified that points to the path with the exchange code.
### Exchange authorization code
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
After redirecting to the server page, we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeCodeForSession` function.
```js
// api/auth/callback.ts
// The code is retrieved from the query parameter - use whichever mechanism is recommended
// for your app/framework. This is just an example.
const code = url.searchParams.get('code')
// call the Supabase API to exchange the code for a session
await supabase.auth.exchangeCodeForSession(code)
```
<Admonition type="note">
The query parameter is always `code` for the authorization code returned from the Supabase API
</Admonition>
We will also need to check for the `next` query parameter to redirect the user to the update password page.
```ts
// api/auth/callback.ts
// The password page path is retrieved from the query parameter - use whichever mechanism is recommended
// for your app/framework. This is just an example.
const next = url.searchParams.get('next')
// using Next.js API response object in this example
res.redirect(next)
```
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
Here is an example on Android:
```kotlin
//Wherever you initialize your SupabaseClient:
val client = createSupabaseClient(supabaseUrl, supabaseKey) {
install(GoTrue) {
flowType = FlowType.PKCE
scheme = "exampleApp"
host = "callback"
}
}
```
```xml
<!-- AndroidManifest.xml -->
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="exampleApp" android:host="callback" />
</intent-filter>
```
```kotlin
//MainActivity.kt
override fun onCreate() {
client.handleDeeplinks(intent)
}
```
That's it! `supabase-kt` will take care of parsing and importing the session. Other targets are similar, you can check [OAuth and OTP link verification](https://supabase.com/docs/reference/kotlin/initializing) for more information.
</TabPanel>
</Tabs>
### Update password
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
To update the password we call the [`.updateUser`](/docs/reference/javascript/auth-updateuser) method and pass along the new password to this method.
```js
await supabase.auth.updateUser({ password: new_password })
```
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
To update the password we call the [`.modifyUser`](/docs/reference/kotlin/auth-updateuser) method and pass along the new password to this method.
```kotlin
supabase.auth.modifyUser {
password = "new_password"
}
```
</TabPanel>
</Tabs>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -98,6 +98,12 @@ dotenv.config()
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 Next.js stack - client, server, App Router, Pages Router. It just works!
<Admonition type="note">
For the client utility functions, it is recommended to create them in a separate `/utils/supabase` folder. That way, you can import them as `client` into any component or page that needs access to Supabase. For further information, you can look at the [Next.js Auth Guide](/docs/guides/auth/server-side/nextjs).
</Admonition>
<Tabs
scrollable
size="small"
@@ -277,7 +277,7 @@ Let's update the URL in our email templates to point to our new confirmation end
<h2>Magic Link</h2>
<p>Follow this link to login:</p>
<p><a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email">Log In</a></p>
<p><a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=magiclink">Log In</a></p>
```
**Change Email Address template**
@@ -155,10 +155,10 @@ export function createClient(cookieStore: ReturnType<typeof cookies>) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
return cookieStore.set({ name, value, ...options })
cookieStore.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
return cookieStore.set({ name, value: '', ...options })
cookieStore.set({ name, value: '', ...options })
},
},
}
@@ -284,9 +284,7 @@ export const config = {
Create a login page for your app. Use a Server Action to call the Supabase signup function.
If you have email confirmations turned on (the default), a new user will receive a confirmation email after signing up. When they click the link, they are directed to the Auth callback route at `auth/confirm` with a secure code. A Route Handler at this route finishes the signup process by exchanging the code for an Auth token.
Since Supabase is being called from an Action and a Route Handler, use the client defined in `@/utils/supabase/actions.ts`.
Since Supabase is being called from an Action, use the client defined in `@/utils/supabase/actions.ts`.
</StepHikeCompact.Details>
@@ -369,7 +367,39 @@ export default function ErrorPage() {
}
```
```ts app/auth/confirm/route.ts
</CH.Code>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={6}>
<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 each email template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=signup`.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={7}>
<StepHikeCompact.Details title="Create a route handler for Auth confirmation">
Create a Route Handler for `auth/callback`. When a user clicks their confirmation email link, exchange their secure code for an Auth token.
Since this is a Router Handler, use the Supabase client from `@/utils/supabase/actions.ts`.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```ts app/auth/callback/route.ts
import { type EmailOtpType } from '@supabase/supabase-js'
import { cookies } from 'next/headers'
import { type NextRequest, NextResponse } from 'next/server'
@@ -408,13 +438,11 @@ export async function GET(request: NextRequest) {
}
```
</CH.Code>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={6}>
<StepHikeCompact.Step step={8}>
<StepHikeCompact.Details title="Access user info from Server Component">
@@ -676,10 +704,10 @@ export default function createClient(req: NextApiRequest, res: NextApiResponse)
return req.cookies[name]
},
set(name: string, value: string, options: CookieOptions) {
res.setHeader('Set-Cookie', serialize(name, value, options))
res.appendHeader('Set-Cookie', serialize(name, value, options))
},
remove(name: string, options: CookieOptions) {
res.setHeader('Set-Cookie', serialize(name, '', options))
res.appendHeader('Set-Cookie', serialize(name, '', options))
},
},
}
@@ -772,11 +800,11 @@ export default function LoginPage() {
<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. By default, the link in this email goes to the path `auth/confirm`.
If you have email confirmation turned on (the default), a new user will receive an email confirmation after signing up.
Change this to `api/auth/confirm`.
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 each email template, change `{{ .SiteURL }}/auth/confirm` to `{{ .SiteURL }}/api/auth/confirm`.
Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In each email template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/api/auth/callback?token_hash={{ .TokenHash }}&type=signup`.
</StepHikeCompact.Details>
@@ -786,7 +814,7 @@ Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templat
<StepHikeCompact.Details title="Create a route handler for Auth confirmation">
Create an API route for `api/auth/confirm`. When a user clicks their confirmation email link, exchange their secure code for an Auth token.
Create an API route for `api/auth/callback`. When a user clicks their confirmation email link, exchange their secure code for an Auth token.
Since this is an API route, use the Supabase client from `@/utils/supabase/api.ts`.
@@ -796,7 +824,7 @@ 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/callback.tsx
import { type EmailOtpType } from '@supabase/supabase-js'
import type { NextApiRequest, NextApiResponse } from 'next'
@@ -200,9 +200,9 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app,
<TabPanel id="flutter" label="Flutter">
## Using native sign in with Apple in Flutter
## Apple sign in on iOS and macOS
You can perform Apple sign in using the [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) package on Flutter.
You can perform Apple sign in using the [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) package on Flutter apps running on iOS or macOS.
Follow the instructions in the package README to set up native Apple sign in on iOS and macOS.
Once the setup is complete on the Flutter app, add the bundle ID of your app to your Supabase dashboard in `Authentication -> Providers -> Apple` in order to register your app with Supabase.
@@ -249,6 +249,32 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app,
</Admonition>
## Apple sign in on Android, Web, Windows and Linux
For platforms that doesn't support native Apple sign in, you can use the `signInWithOAuth()` method to perform the Apple sign in.
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).
```dart
supabase.auth.signInWithOAuth(
OAuthProvider.apple,
redirectTo: 'my-scheme://login-callback',
);
```
This call takes the user to Apple's consent screen. Once the flow ends, the user's profile information is exchanged and validated with Supabase Auth before it redirects back to your Flutter application with an access and refresh token representing the user's session.
### Configuration [#flutter-configuration-web]
You will require the following information:
1. Your Apple Developer account's **Team ID**, which is an alphanumeric string of 10 characters that uniquely identifies the developer of the app. It's often easily accessible in the upper right-side menu on the Apple Developer Console.
2. Register email sources for _Sign in with Apple for Email Communication_ which can be found in the [Services](https://developer.apple.com/account/resources/services/list) section of the Apple Developer Console.
3. An **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple once you create an App ID in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as _bundle IDs._)
4. A **Services ID** which uniquely identifies the web services provided by the app you registered in the previous step. You can create a new Services ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/serviceId) section in the Apple Developer Console (use the filter menu in the upper right side to see all Services IDs). These usually are a reverse domain name string, for example `com.example.app.web`.
5. Configure Website URLs for the newly created **Services ID**. The web domain you should use is the domain your Supabase project is hosted on. This is usually `<project-id>.supabase.co` while the redirect URL is `https://<project-id>.supabase.co/auth/v1/callback`.
6. Create a signing **Key** in the [Keys](https://developer.apple.com/account/resources/authkeys/list) section of the Apple Developer Console. You can use this key to generate a secret key using the tool below, which is added to your Supabase project's Auth configuration. Make sure you safely store the `AuthKey_XXXXXXXXXX.p8` file. If you ever lose access to it, or make it public accidentally please revoke it from the Apple Developer Console and create a new one immediately. You will have to generate a new secret key using this file every 6 months, so make sure you schedule a recurring reminder in your calendar!
7. Finally, add the information you configured above to the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers).
</TabPanel>
<TabPanel id="swift" label="Swift">
@@ -52,6 +52,14 @@ Under `Redirect URIs`:
## Add login code to your client app
The following outlines the steps to sign in using Spotify with Supabase Auth.
1. Call the signin method from the client library.
1. The user is redirected to the Spotify login page.
1. After completing the sign-in process, the user will be redirected to your app with an error that says the email address needs to be confirmed. Simultaneously the user receives a confirmation email from Supabase Auth.
1. The user clicks the confirmation link in the email.
1. The user is brought back to the app and is now signed in.
<Tabs
scrollable
size="small"
@@ -12,12 +12,12 @@ export const meta = {
Supabase provides several options for programmatically connecting to your Postgres database:
1. Programmatic access using the Data APIs
1. Direct connections using the built-in Postgres connection system
1. Connection pooling for scalable connections
1. Direct connections using the built-in Postgres connection system
<Admonition type="caution">
Starting on <DateFormat date="2024-01-26" options={{ dateStyle: "long", timeZone: "UTC" }} />, direct database connections will start resolving to an IPv6 address. If you connect to your database using the REST and GraphQL endpoints, this doesn't affect you. If you use the database connection string, see the GitHub discussion on [how to prepare for IPv6](https://github.com/orgs/supabase/discussions/17817).
Direct database connections will start resolving to an IPv6 address. Starting from January 26, 2024, projects will be gradually migrated to this behavior. If you connect to your database using the REST or GraphQL endpoints, this doesn't affect you. If you use the database connection string, see the GitHub discussion on [how to prepare for IPv6](https://github.com/orgs/supabase/discussions/17817).
</Admonition>
@@ -31,37 +31,61 @@ Supabase provides auto-updating Data APIs. These are the easiest way to get star
## Connection pooler
Every Supabase project comes with a connection pooler for managing connections to your Postgres database.
A "connection pool" is a system (external to Postgres) which manages Postgres connections.
A connection pooler is useful for managing a large number of _temporary_ connections - for example, if you are using Prisma, Drizzle, Kysely, or anything deployed to a Serverless environment (AWS Lambdas or Edge Functions). Supabase's connection pooler also supports ipv4 out of the box.
When a client makes a request, the pooler "allocates" an available connection to the client. When the client transaction or session is completed the connection is returned to the pool and is free to be used by another client.
<figure>
<Image
className="hidden dark:block"
alt="New migration files trigger migrations on the preview instance."
src="/docs/img/guides/database/connecting-to-postgres/how-connection-pooling-works.png"
/>
<Image
className="dark:hidden"
alt="New migration files trigger migrations on the preview instance."
src="/docs/img/guides/database/connecting-to-postgres/how-connection-pooling-works--light.png"
/>
<figcaption>Connecting to the database directly vs using a Connection Pooler</figcaption>
</figure>
Every Supabase project comes with a connection pooler for managing connections to your database. The pooler provides 2 important services:
1. It manages connections for applications that connect and disconnect from the database frequently. For example, serverless functions and ORMs such as Prisma, Drizzle, and Kysely often make and drop connections to the database. If they connected directly each time, they would quickly exhaust your database server's memory. To connect to your database efficiently with such tools, you need a pooler.
2. It provides an IPv4 endpoint to connect to your database, which you can use if your network provider doesn't support IPv6. This is unlike the direct database connection, which resolves to an IPv6 address, unless you've enabled the [IPv4 add-on](https://supabase.com/docs/guides/platform/ipv4-address).
<Admonition type="tip">
The connection pooler is available in 2 modes: `Transaction` and `Session`.
</Admonition>
Transaction mode is recommended if you are connecting from serverless environments. A connection is assigned to the client for the duration of a transaction. Two consecutive transactions from the same client can be executed over two different connections. Some session-based Postgres features such as prepared statements are _not available_ with this option.
Session mode is similar to connecting to your database directly. There is full support for prepared statements in this mode. When a new client connects, a connection is assigned to the client until it disconnects. You might run into pooler connection limits since the connection is held till the client disconnects.
You can find the connection pool config in the [Database settings](/dashboard/project/_/settings/database) inside the dashboard:
1. Go to the `Settings` section.
2. Click `Database`.
3. Under `Connect to your database via connection pooling`, copy your `Connection string`.
3. Under `Connection string`, make sure `Use connection pooling` is checked. Copy the URI.
## Direct connections
You can use a direct connection to connect directly to your Postgres database. By default, this connection uses ipv6, which isn't supported by all network providers. If you need an ipv4 address, use the [connection pooler](#connection-pooler) instead.
You can also use a direct connection to connect directly to your Postgres database. By default, you can connect to it via IPv6, which isn't supported by all networks. If you need an IPv4 address, use the [connection pooler](#connection-pooler) instead or enable the [IPv4 add-on](https://supabase.com/docs/guides/platform/ipv4-address).
You can find the direct connection string in the [Database settings](https://supabase.com/dashboard/project/_/settings/database) inside the dashboard:
1. Go to the `Settings` section.
2. Click `Database`.
3. Under `Connect to your database directly`, copy your `Connection string`.
3. Under `Connection string`, make sure `Use connection pooling` is unchecked. Copy the URI.
## Choosing a connection method
- The Data APIs provide programmatic access and have [built-in connection pooling](https://postgrest.org/en/stable/references/connection_pool.html). You can use these for all browser and application interactions. We recommend using these wherever possible.
- A "connection pooler" is a tool which keeps connections "alive". You should use this for serverless functions and tools which disconnect from the database frequently, like Prisma, Drizzle, Kysely, etc. You should also use this if your network doesn't support ipv6.
- A "direct connection" is Postgres' native connection system. You can use this for tools which are always alive, such as long-running server, as long as your network supports ipv6.
Why would you use a connection pool? Primarily because the way that Postgres handles connections isn't very scalable for a large number of _temporary_ connections. You can use these simple questions to determine which connection method to use:
- Are you on a network that doesn't support ipv6? Use the connection pooler.
- Are you connecting to your database and then _disconnecting_ immediately (e.g. a serverless environment)? Use the connection pooler.
- Are you connecting to a database and _maintaining_ a connection, and does your network support ipv6? If yes, use a direct connection.
- Are you performing operations supported by the Data APIs? We recommend using the APIs.
- Are you connecting to your database and then _disconnecting_ immediately (for example, are you running your code in a serverless environment)? Use the connection pooler in transaction mode.
- Are you on a network that doesn't support IPv6? Use the connection pooler. (If you need a long-lived connection, for example for prepared statements, use session mode.)
- Are you connecting to your database and _maintaining_ a connection, and does your network support IPv6? Use a direct connection.
## Connecting with SSL
@@ -108,7 +132,7 @@ You can obtain your connection info and Server root certificate from your applic
<StepHikeCompact.Code>
```ts
```ts schema.ts
import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core";
export const users = pgTable('users', {
@@ -126,24 +150,25 @@ You can obtain your connection info and Server root certificate from your applic
<StepHikeCompact.Details title="Connect">
Connect to your database using the Connection Pooler for serverless environments, and the Direct Connection for long-running servers.
Connect to your database using the Connection Pooler.
In your [`Database Settings`](https://supabase.com/dashboard/project/_/settings/database), make sure `Use connection pooler` is checked, then copy the URI and save it as the `DATABASE_URL` environment variable. Remember to replace the password placeholder with your actual database password.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```ts
```ts db.ts
import 'dotenv/config'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { users } from './schema'
const connectionString = process.env.DATABASE_URL
// Disable prefetch as it is not supported for "Transaction" pool mode
const client = postgres(connectionString, { prepare: false })
const db = drizzle(client);
const allUsers = await db.select().from(users);
export const client = postgres(connectionString, { prepare: false })
export const db = drizzle(client);
```
</StepHikeCompact.Code>
@@ -168,7 +193,16 @@ You can obtain your connection info and Server root certificate from your applic
<StepHikeCompact.Code>
![Register a new postgres server.](/docs/img/guides/database/register-server-pgAdmin.png)
<Image
className="hidden dark:block"
alt="Register a new postgres server."
src="/docs/img/guides/database/connecting-to-postgres/pgadmin/register-server-pgAdmin.png?v=2"
/>
<Image
className="dark:hidden"
alt="Register a new postgres server."
src="/docs/img/guides/database/connecting-to-postgres/pgadmin/register-server-pgAdmin--light.png"
/>
</StepHikeCompact.Code>
@@ -184,7 +218,7 @@ You can obtain your connection info and Server root certificate from your applic
<StepHikeCompact.Code>
![Name Postgres Server.](/docs/img/guides/database/name-pg-server.png)
![Name Postgres Server.](/docs/img/guides/database/connecting-to-postgres/pgadmin/name-pg-server.png)
</StepHikeCompact.Code>
@@ -194,13 +228,13 @@ You can obtain your connection info and Server root certificate from your applic
<StepHikeCompact.Details title="Connect">
Add the connection info. You can use the "Direct connection" config, which you can find in your Supabase dashboard.
Add the connection info. Go to your [`Database Settings`](https://supabase.com/dashboard/project/_/settings/database). Make sure `Use connection pooling` is enabled. Switch the connection mode to `Session` and copy your connection parameters.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
![Add Connection Info.](/docs/img/guides/database/add-pg-server-conn-info.png)
![Add Connection Info.](/docs/img/guides/database/connecting-to-postgres/pgadmin/add-pg-server-conn-info.png)
</StepHikeCompact.Code>
@@ -216,7 +250,7 @@ You can obtain your connection info and Server root certificate from your applic
<StepHikeCompact.Code>
![Add Connection Info.](/docs/img/guides/database/add-ssl-config-parameter.png)
![Add Connection Info.](/docs/img/guides/database/connecting-to-postgres/pgadmin/database-settings-host.png)
</StepHikeCompact.Code>
@@ -228,10 +262,12 @@ You can obtain your connection info and Server root certificate from your applic
[`psql`](https://www.postgresql.org/docs/current/app-psql.html) is a command-line tool that comes with Postgres.
Assuming you've downloaded your SSL certificate to `$HOME/Downloads/prod-supabase.cer`, and your host address is `db.ref.supabase.co` you connect to your database via SSL:
Download your [SSL certificate](#connecting-with-ssl) to `/path/to/prod-supabase.cer`.
Find your connection settings. Go to your [`Database Settings`](https://supabase.com/dashboard/project/_/settings/database) and make sure `Use connection pooling` is checked. Change the connection mode to `Session`, and copy the parameters into the connection string:
```shell
psql "sslmode=verify-full sslrootcert=$HOME/Downloads/prod-supabase.cer host=db.ref.supabase.co dbname=postgres user=postgres"
psql "sslmode=verify-full sslrootcert=/path/to/prod-supabase.cer host=[CLOUD_PROVIDER]-0-[REGION].pooler.supabase.com dbname=postgres user=postgres.[PROJECT_REF]"
```
### Connecting with Prisma
@@ -266,7 +302,9 @@ Refer to our [Prisma integration guide](https://supabase.com/partners/integratio
<StepHikeCompact.Details title="Connect">
Create a `db.js` file with the connection details. Use the Connection Pooler for serverless environments, and the Direct Connection for long-running servers.
Create a `db.js` file with the connection details.
To get your connection details, go to your [`Database Settings`](https://supabase.com/dashboard/project/_/settings/database). Make sure `Use connection pooling` is enabled. Choose `Transaction Mode` if you're on a platform with transient connections, such as a serverless function, and `Session Mode` if you have a long-lived connection. Copy the URI and save it as the environment variable `DATABASE_URL`.
</StepHikeCompact.Details>
@@ -316,39 +354,30 @@ Refer to our [Prisma integration guide](https://supabase.com/partners/integratio
</StepHikeCompact>
## How connection pooling works
## Troubleshooting Supavisor
A "connection pool" is a system (external to Postgres) which manages Postgres connections.
Connection pooler logs are found [here](https://supabase.com/dashboard/project/_/logs/pooler-logs). The following are common errors and their solutions:
When a client makes a request, the pooler "allocates" an available connection to the client. When the client transaction or session is completed the connection is returned to the pool and is free to be used by another client.
- Max client connections reached
This error happens when the number of connections to Supavisor is more than [the allowed limit of your compute add-on](https://supabase.com/docs/guides/platform/compute-add-ons). Upgrade the database to a higher compute add-on to increase the number of Supavisor connections.
![Connection pooling](/docs/img/guides/database/connection-pool.png)
- Connection failed \{:error, :eaddrnotavail} to 'db.xxx.supabase.co':5432
Supavisor cannot connect to the customer database. This is usually caused if the target database is unable to respond.
There are several pool modes, each handling connections differently:
- Connection failed \{:error, :nxdomain} to 'db.xxx.supabase.co':5432
Supavisor cannot connect to the customer database. This is usually caused if the target database is unable to respond.
### Session
- Connection closed when state was authentication
This error happens when either the database doesn’t exist or if the user doesn't have the right credentials.
When a new client connects, a connection is assigned to the client until it disconnects. Afterward, the connection is returned back to the pool.
- Subscribe error: \{:error, :worker_not_found}
This log event is emitted when the client tries to connect to the database, but Supavisor does not have the necessary information to route the connection. Try reconnecting to the database as it can take some time for the project information to propagate to Supavisor.
All Postgres features can be used with this option.
- Subscribe error: \{:error, \{:badrpc, \{:error, \{:erpc, :timeout}}}}
This is a timeout error when the communication between different Supavisor nodes takes longer than expected. Try reconnecting to the database.
### Transaction
This is the suggested option for serverless functions. A connection is assigned to the client for the duration of a transaction. Two consecutive transactions from the same client could be executed over two different connections. Some session-based Postgres features such as prepared statements are not available with this option.
### Statement
This is the most granular option. Connections are returned to the pool after every statement. Transactions with multiple statements are not allowed. This is best used when `AUTOCOMMIT` is in use.
### Supavisor vs PgBouncer
Supabase previously used PgBouncer for connection pooling. We have now deprecated PgBouncer in favor of Supavisor. Supavisor is available on all new and existing projects.
[Supavisor](https://github.com/supabase/supavisor) is a new connection pooler by Supabase that runs on a high-availability cluster, segregated from your database. This means more resources are available for your database. No Application changes are required to switch from PgBouncer to Supavisor, you simply need to choose the new connection string from the "Connection Pooling" section on [Database settings](https://supabase.com/dashboard/project/_/settings/database).
PgBouncer is now deprecated and is in the process of being removed from the platform, you can [see full details and timelines of the deprecation on Github Discussions](https://github.com/orgs/supabase/discussions/17817). Additionally, your Supabase database domain (db.projectref.supabase.co) will start resolving to an IPv6 address. No changes are required if your network supports IPv6. Otherwise, update your applications to use Supavisor which will continue to support IPv4 connections.
[Read the full pgBouncer and IPv4 deprecation details](https://github.com/orgs/supabase/discussions/17817).
- 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.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -6,7 +6,7 @@ export const meta = {
description: 'Automatically optimize SQL queries',
}
[Index advisor](https://github.com/supabase/index_advisor) is a Postgres extension for recommending indexes to improve query performance.
[Index advisor](https://database.dev/olirice/index_advisor) is a Postgres extension for recommending indexes to improve query performance.
For example:
@@ -154,7 +154,8 @@ from
## Resources
- Official [`index_advisor` GitHub Repository](https://github.com/supabase/index_advisor)
- dbdev [`index_advisor`](https://database.dev/olirice/index_advisor) docs
- dbdev [Github Repository](https://github.com/supabase/dbdev)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -88,7 +88,6 @@ create table contacts (
id uuid default uuid_generate_v4(),
first_name text,
last_name text,
primary key (id)
);
```
@@ -8,7 +8,7 @@ export const meta = {
Your Postgres database is the core of your Supabase project, so it's important that it has a strong, secure password at all times.
If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string e.g. `postgresql://postgres:p%3Dword@db.cvwawazfelidkloqmbma.supabase.co:5432/postgres`
If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string e.g. `postgresql://postgres.projectref:p%3Dword@aws-0-us-east-1.pooler.supabase.com:6543/postgres`
## Changing your project password
@@ -157,7 +157,7 @@ partition by list(country);
create table customers_americas
partition of customers
for values in ('US', 'CANADA')
for values in ('US', 'CANADA');
create table customers_asia
partition of customers
@@ -40,7 +40,7 @@ Your Postgres database is the core of your Supabase project, so it's important t
### Special symbols in passwords
If you use special symbols in your Postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the Postgres connection string e.g. `postgresql://postgres:p%3Dword@db.xxx.supabase.co:5432/postgres`.
If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string e.g. `postgresql://postgres.projectref:p%3Dword@aws-0-us-east-1.pooler.supabase.com:6543/postgres`
### Changing your project password
@@ -13,7 +13,7 @@ In Postgres, a trigger executes a set of actions automatically on table events s
Creating triggers involve 2 parts:
1. A [Function](docs/guides/database/functions) which will be executed (called the Trigger Function)
1. A [Function](/docs/guides/database/functions) which will be executed (called the Trigger Function)
2. The actual Trigger object, with parameters around when the trigger should be run.
An example of a trigger is:
@@ -27,7 +27,7 @@ execute function trigger_function();
## Trigger functions
A trigger function is a user-defined [Function](docs/guides/database/functions) that Postgres executes when the trigger is fired.
A trigger function is a user-defined [Function](/docs/guides/database/functions) that Postgres executes when the trigger is fired.
### Example trigger function
@@ -13,7 +13,7 @@ Select a set of columns from a single table with where, order by, and limit clau
```sql
select first_name, last_name, team_id, age
from players
where age between 20 and 24 and team_id <> 'STL'
where age between 20 and 24 and team_id != 'STL'
order by last_name, first_name desc
limit 20;
```
@@ -52,7 +52,7 @@ Select all columns from a single table with a complex where clause: AND OR AND
```sql
select *
from players
where ((team_id = 'CHN' and age > 35) or (team_id <> 'CHN' and age is not null));
where ((team_id = 'CHN' and age > 35) or (team_id != 'CHN' and age is not null));
```
```js
+1 -1
View File
@@ -5,7 +5,7 @@ export const meta = {
title: 'Deploy to Production',
description: 'Deploy your Edge Functions to your remote Supabase Project.',
subtitle: 'Deploy your Edge Functions to your remote Supabase Project.',
tocVideo: 'rzglqRdZUQE',
tocVideo: '5OWH9c4u68M',
}
Once you have developed your Edge Functions locally, you can deploy them to your Supabase project.
@@ -17,6 +17,12 @@ You can follow the [Deno guide](https://deno.com/manual@v1.32.5/getting_started/
When using VSCode, you should install both the Deno CLI and the the Deno language server [via this link](vscode:extension/denoland.vscode-deno) or by browsing the extensions in vscode and choosing to install the _Deno_ extension.
<Admonition type="tip">
The Supabase CLI can automatically create helpful Deno settings when running `supabase init`. Simply select `y` when prompted "Generate VS Code settings for Deno? [y/N]"!
</Admonition>
## Deno support in subfolders
You can enable the Deno language server for specific sub-paths in a workspace, while using VSCode's built-in JavaScript/TypeScript language server for all other files.
@@ -41,7 +47,7 @@ To enable the Deno language server only for the `supabase/functions` folder, add
## Multi-root workspaces in VSCode
When developing with VS Code inside of an existing application, you can utilize [multi-root workspaces](https://code.visualstudio.com/docs/editor/workspaces#_multiroot-workspaces).
We recommend using `deno.enablePaths` mentioned above as it's easier to manage, however if you like [multi-root workspaces](https://code.visualstudio.com/docs/editor/workspaces#_multiroot-workspaces) you can use these as an alternative.
<div className="video-container">
<iframe
@@ -5,7 +5,7 @@ export const meta = {
title: 'Developing Edge Functions locally',
description: 'Get started with Edge Functions on your local machine.',
subtitle: 'Get started with Edge Functions on your local machine.',
tocVideo: 'rzglqRdZUQE',
tocVideo: '5OWH9c4u68M',
}
Let's create a basic Edge Function on your local machine and then invoke it using the Supabase CLI.
@@ -24,6 +24,12 @@ Check out the [CLI Docs](/docs/guides/cli) to learn how to install the Supabase
</Admonition>
<Admonition type="tip">
If you're using VS code you can have the CLI automatically create helpful Deno settings when running `supabase init`. Simply select `y` when prompted "Generate VS Code settings for Deno? [y/N]"!
</Admonition>
## Create an Edge Function
Let's create a new Edge Function called `hello-world` inside your project:
@@ -52,9 +52,13 @@ export const meta = {
Go to [database.new](https://database.new) and create a new Supabase project. Save your database password securely.
When your project is up and running, navigate to the [database settings](https://supabase.com/dashboard/project/_/settings/database) to find the URI connection string.
When your project is up and running, navigate to the [database settings](https://supabase.com/dashboard/project/_/settings/database) to find the URI connection string. Make sure **Use connection pooling** is checked and **Session mode** is selected. Then copy the URI. Replace the password placeholder with your saved database password.
Laravel ships with a Postgres adapter out of the box, you can simply configure it via the environment variables. You can find the database URL in your [Supabase Dashboard](/dashboard/project/_/settings/database).
<Admonition type="tip">
If your network supports IPv6 connections, you can also use the direct connection string. Uncheck **Use connection pooling** and copy the new URI.
</Admonition>
</StepHikeCompact.Details>
@@ -62,7 +66,7 @@ export const meta = {
```bash .env
DB_CONNECTION=pgsql
DATABASE_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres
DATABASE_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres
```
</StepHikeCompact.Code>
@@ -123,7 +127,7 @@ export const meta = {
<StepHikeCompact.Step step={6}>
<StepHikeCompact.Details title="Start the app">
Run the development server. Go to http://127.0.0.1:8000 in a browser to see your application. You can also nvaigate to http://127.0.0.1:8000/register and http://127.0.0.1:8000/login to register and log in users.
Run the development server. Go to http://127.0.0.1:8000 in a browser to see your application. You can also navigate to http://127.0.0.1:8000/register and http://127.0.0.1:8000/login to register and log in users.
</StepHikeCompact.Details>
@@ -34,16 +34,20 @@ export const meta = {
Go to [database.new](https://database.new) and create a new Supabase project. Save your database password securely.
When your project is up and running, navigate to the [database settings](https://supabase.com/dashboard/project/_/settings/database) to find the URI connection string.
When your project is up and running, navigate to the [database settings](https://supabase.com/dashboard/project/_/settings/database) to find the URI connection string. Make sure **Use connection pooling** is checked and **Session mode** is selected. Then copy the URI. Replace the password placeholder with your saved database password.
Rails ships with a Postgres adapter included, you can simply configure it via the environment variables. You can find the database URL in your [Supabase Dashboard](/dashboard/project/_/settings/database).
<Admonition type="tip">
If your network supports IPv6 connections, you can also use the direct connection string. Uncheck **Use connection pooling** and copy the new URI.
</Admonition>
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash Terminal
export DATABASE_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres
export DATABASE_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres
```
</StepHikeCompact.Code>
@@ -230,7 +230,10 @@ export class AccountComponent implements OnInit {
avatar_url: '',
})
constructor(private readonly supabase: SupabaseService, private formBuilder: FormBuilder) {}
constructor(
private readonly supabase: SupabaseService,
private formBuilder: FormBuilder
) {}
async ngOnInit(): Promise<void> {
await this.getProfile()
@@ -421,7 +424,10 @@ export class AvatarComponent {
@Output() upload = new EventEmitter<string>()
constructor(private readonly supabase: SupabaseService, private readonly dom: DomSanitizer) {}
constructor(
private readonly supabase: SupabaseService,
private readonly dom: DomSanitizer
) {}
async downloadImage(path: string) {
try {
@@ -451,10 +451,10 @@ photos and videos.
### Additional dependency installation
You will need a file picker that works on the environment you will build the project for, we will use react-native-document-picker in this example.
You will need an image picker that works on the environment you will build the project for, we will use `expo-image-picker` in this example.
```bash
expo install react-native-document-picker
expo install expo-image-picker
```
### Create an upload widget
@@ -466,7 +466,7 @@ We can start by creating a new component:
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabase'
import { StyleSheet, View, Alert, Image, Button } from 'react-native'
import DocumentPicker, { isCancel, isInProgress, types } from 'react-native-document-picker'
import * as ImagePicker from 'expo-image-picker'
interface Props {
size: number
@@ -507,39 +507,43 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
try {
setUploading(true)
const file = await DocumentPicker.pickSingle({
presentationStyle: 'fullScreen',
copyTo: 'cachesDirectory',
type: types.images,
mode: 'open',
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images, // Restrict to only images
allowsMultipleSelection: false, // Can only select one image
allowsEditing: true, // Allows the user to crop / rotate their photo before uploading it
quality: 1,
exif: false, // We don't want nor need that data.
})
const photo = {
uri: file.fileCopyUri,
type: file.type,
name: file.name,
if (result.canceled || !result.assets || result.assets.length === 0) {
console.log('User cancelled image picker.')
return
}
const formData = new FormData()
formData.append('file', photo)
const image = result.assets[0]
console.log('Got image', image)
const fileExt = file.name.split('.').pop()
const filePath = `${Math.random()}.${fileExt}`
const { error } = await supabase.storage.from('avatars').upload(filePath, formData)
if (error) {
throw error
if (!image.uri) {
throw new Error('No image uri!') // Realistically, this should never happen, but just in case...
}
onUpload(filePath)
const arraybuffer = await fetch(image.uri).then((res) => res.arrayBuffer())
const fileExt = image.uri?.split('.').pop()?.toLowerCase() ?? 'jpeg'
const path = `${Date.now()}.${fileExt}`
const { data, error: uploadError } = await supabase.storage
.from('avatars')
.upload(path, arraybuffer, {
contentType: image.mimeType ?? 'image/jpeg',
})
if (uploadError) {
throw uploadError
}
onUpload(data.path)
} catch (error) {
if (isCancel(error)) {
console.warn('cancelled')
// User cancelled the picker, exit any dialogs or menus and move on
} else if (isInProgress(error)) {
console.warn('multiple pickers were opened, only the last will be considered')
} else if (error instanceof Error) {
if (error instanceof Error) {
Alert.alert(error.message)
} else {
throw error
@@ -583,7 +587,9 @@ const styles = StyleSheet.create({
},
noImage: {
backgroundColor: '#333',
border: '1px solid rgb(200, 200, 200)',
borderWidth: 1,
borderStyle: 'solid',
borderColor: 'rgb(200, 200, 200)',
borderRadius: 5,
},
})
@@ -598,23 +604,22 @@ And then we can add the widget to the Account page:
import Avatar from './Avatar'
// ...
return (
return (
<View>
{/* Add to the body */}
<View>
{/* Add to the body */}
<View>
<Avatar
size={200}
url={avatarUrl}
onUpload={(url: string) => {
setAvatarUrl(url)
updateProfile({ username, website, avatar_url: url })
}}
/>
</View>
{/* ... */}
<Avatar
size={200}
url={avatarUrl}
onUpload={(url: string) => {
setAvatarUrl(url)
updateProfile({ username, website, avatar_url: url })
}}
/>
</View>
)
}
{/* ... */}
</View>
)
// ...
```
@@ -70,7 +70,10 @@ export interface Profile {
export class SupabaseService {
private supabase: SupabaseClient
constructor(private loadingCtrl: LoadingController, private toastCtrl: ToastController) {
constructor(
private loadingCtrl: LoadingController,
private toastCtrl: ToastController
) {
this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)
}
@@ -252,7 +255,10 @@ export class AccountPage implements OnInit {
email = ''
constructor(private readonly supabase: SupabaseService, private router: Router) {}
constructor(
private readonly supabase: SupabaseService,
private router: Router
) {}
ngOnInit() {
this.getEmail()
this.getProfile()
@@ -319,7 +325,10 @@ import { SupabaseService } from './supabase.service'
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor(private supabase: SupabaseService, private router: Router) {
constructor(
private supabase: SupabaseService,
private router: Router
) {
this.supabase.authChanges((_, session) => {
console.log(session)
if (session?.user) {
@@ -474,7 +483,10 @@ export class AvatarComponent {
@Output() upload = new EventEmitter<string>()
constructor(private readonly supabase: SupabaseService, private readonly dom: DomSanitizer) {
constructor(
private readonly supabase: SupabaseService,
private readonly dom: DomSanitizer
) {
addIcons({ person })
}
@@ -121,11 +121,11 @@ Let's create a new component for that called `Account.svelte`.
```html src/lib/Account.svelte
<script lang="ts">
import { onMount } from "svelte";
import type { AuthSession } from "@supabase/supabase-js";
import { supabase } from "../supabaseClient";
import { onMount } from 'svelte'
import type { AuthSession } from '@supabase/supabase-js'
import { supabase } from '../supabaseClient'
export let session: AuthSession;
export let session: AuthSession
let loading = false
let username: string | null = null
@@ -191,23 +191,23 @@ Let's create a new component for that called `Account.svelte`.
}
</script>
<form on:submit|preventDefault={updateProfile} class="form-widget">
<form on:submit|preventDefault="{updateProfile}" class="form-widget">
<div>Email: {session.user.email}</div>
<div>
<label for="username">Name</label>
<input id="username" type="text" bind:value={username} />
<input id="username" type="text" bind:value="{username}" />
</div>
<div>
<label for="website">Website</label>
<input id="website" type="text" bind:value={website} />
<input id="website" type="text" bind:value="{website}" />
</div>
<div>
<button type="submit" class="button primary block" disabled={loading}>
<button type="submit" class="button primary block" disabled="{loading}">
{loading ? 'Saving ...' : 'Update profile'}
</button>
</div>
<button type="button" class="button block" on:click={() => supabase.auth.signOut()}>
Sign Out
<button type="button" class="button block" on:click="{()" ="">
supabase.auth.signOut()}> Sign Out
</button>
</form>
```
@@ -16,6 +16,12 @@ export const mutuallyExclusiveNote = (
Database backups are an integral part of any disaster recovery plan. Disasters come in many shapes and sizes. It could be as simple as accidentally deleting a table column, the database crashing, or even a natural calamity wiping out the underlying hardware a database is running on. The risks and impact brought by these scenarios can never be fully eliminated, but only minimized or even mitigated. Having database backups is a form of insurance policy. They are essentially snapshots of the database at various points in time. When disaster strikes, database backups allow the project to be brought back to any of these points in time, therefore averting the crisis.
<Admonition type="note">
The Supabase team regularly monitors the status of backups. In case of any issues, you can [contact support](https://supabase.com/dashboard/support/new). Also you can check out our [status page](https://status.supabase.com/) at any time.
</Admonition>
## Frequency of backups
When deciding how often a database should be backed up, the key business metric Recovery Point Objective (RPO) should be considered. RPO is the threshold for how much data, measured in time, a business could lose when disaster strikes. This amount is fully dependent on a business and its underlying requirements. A low RPO would mean that database backups would have to be taken at an increased cadence throughout the day. Each Supabase project has access to two forms of backups, Daily Backups and Point-in-Time Recovery (PITR). The agreed upon RPO would be a deciding factor in choosing which solution best fits a project.
@@ -149,7 +149,7 @@ You can use the [Supabase CLI](/docs/guides/cli) to manage changes inside a loca
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Pull your database migration" fullWidth>
Pull your database changes using `supabase db pull`. You can find your database URL in your [database settings](https://supabase.com/dashboard/project/_/settings/database), under the URI tab of the Connection String settings panel.
Pull your database changes using `supabase db pull`. You can find your database URL in your [database settings](https://supabase.com/dashboard/project/_/settings/database), under the URI tab of the Connection String settings panel. Make sure **Use connection pooling** is checked so you can use the IPv4-enabled connection pooler. (Without connection pooling, your database is only accessible over IPv6, which isn't yet supported by all network providers.)
<CH.Code lineNumbers={false}>
@@ -123,13 +123,7 @@ Migrating projects can be achieved using the Supabase CLI. This is particularly
- Create a new [Supabase project](https://supabase.com/dashboard).
- Install [Docker Desktop](https://www.docker.com) for your platform.
- Set environment variables for the old project's database URL as `$OLD_DB_URL` and the new project's as `$NEW_DB_URL`.
To find the database URL for a project, go to the project's dashboard page [Project Settings/Database](https://supabase.com/dashboard/project/_/settings/database) and look at `Connection string / URI`. For example, to set the `$OLD_DB_URL` you would run `export OLD_DB_URL=postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF#].supabase.co:5432/postgres`.
<Admonition type="tip">
Note that this direct connection string to the database uses IPv6. If your network provider doesn't yet support IPv6, you can connect via the pooler connection string, available under the **Connection pooling** section of [**Database settings**](https://supabase.com/dashboard/project/_/settings/database). The connection string has the format `postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres`.
</Admonition>
To find the database URL for a project, go to the project's dashboard page [Project Settings/Database](https://supabase.com/dashboard/project/_/settings/database) and look under `Connection string`. If your network provider supports IPv6, you can disable `Use connection pooling`. Otherwise, enable `Use connection pooling`. For the pooler mode, `Transaction` will work.
### Backup your old database
@@ -172,7 +166,6 @@ psql \
--file schema.sql \
--command 'SET session_replication_role = replica' \
--file data.sql \
--file history.sql \
--dbname "$NEW_DB_URL"
```
@@ -77,8 +77,7 @@ Restrictions applied successfully: true
## Limitations
- The current iteration of Network Restrictions applies to connections to Postgres and the database pooler; it doesn't currently apply to APIs offered over HTTPS (e.g., PostgREST, Storage, and Auth). This includes using Supabase client libraries like [supabase-js](https://supabase.com/docs/reference/javascript).
- Network Restrictions should not be used if you need to connect to your Postgres database using Edge Functions.
- [Supavisor](https://github.com/supabase/supavisor/) does not currently support Network Restrictions. Network restrictions support will be enabled from 24th January 2024. Any changes to the network restrictions configuration will apply to both Supavisor and direct connections to Postgres after this date.
- If network restrictions are enabled, direct access to your database from Edge Functions will always be blocked. Using the Supabase client library [supabase-js](https://supabase.com/docs/reference/javascript) is recommended to connect to a database with network restrictions from Edge Functions.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -58,10 +58,12 @@ Alternatively, you can use [pgloader](https://github.com/dimitri/pgloader), a fl
Install pgloader.
Create a configuration file (e.g., config.load).
For your destination, use your Supabase connection string with `Use connection pooling` enabled, and the mode set to `Session`. You can get the string from your [`Database Settings`](https://supabase.com/dashboard/project/_/settings/general).
```sql
load database
from mysql://user:password@host/source_db
into postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres
into postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres
alter schema 'public' owner to 'postgres';
set wal_buffers = '64MB', max_wal_senders = 0, statement_timeout = 0, work_mem to '2GB';
```
@@ -33,7 +33,7 @@ git clone https://github.com/supabase-community/firebase-to-supabase.git
```
1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard.
1. Under **Connection Info**, copy the Host string and replace the entry in your `supabase-service.json` file.
1. Under `Connection parameters`, enable `Use connection pooling` and set the mode to `Session`. Replace the `Host` and `User` fields with the values shown.
1. Enter the password you used when you created your Supabase project in the `password` entry in the `supabase-service.json` file.
## Generate a Firebase private key [#generate-firebase-private-key]
@@ -32,7 +32,7 @@ git clone https://github.com/supabase-community/firebase-to-supabase.git
```
1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard.
1. Under **Connection Info**, copy the Host string and replace the entry in your `supabase-service.json` file.
1. Under `Connection parameters`, enable `Use connection pooling` and set the mode to `Session`. Replace the `Host` and `User` fields with the values shown.
1. Enter the password you used when you created your Supabase project in the `password` entry in the `supabase-service.json` file.
## Generate a Firebase private key [#generate-firebase-private-key]
@@ -34,11 +34,11 @@ Alternatively, use the [Heroku to Supabase migration tool](https://migrate.supab
- User (`$HEROKU_USER`)
- Password (`$HEROKU_PASSWORD`)
## Retrieve your Supabase host [#retrieve-supabase-host]
## Retrieve your Supabase connection string [#retrieve-supabase-connection-string]
1. If you're new to Supabase, [create a project](https://supabase.com/dashboard).
1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard.
1. Under **Connection Info**, note your Host (`$SUPABASE_HOST`).
1. Under **Connection string**, make sure `Use connection pooling` is enabled. Copy the URI and replace the password placeholder with your database password.
## Export your Heroku database to a file [#export-heroku-database]
@@ -55,7 +55,7 @@ pg_dump --clean --if-exists --quote-all-identifiers \
Use `psql` to import the Heroku database file to your Supabase project.
```bash
psql -h $SUPABASE_HOST -U postgres -f heroku_dump.sql
psql -d "$YOUR_CONNECTION_STRING" -f heroku_dump.sql
```
## Additional options
@@ -55,10 +55,12 @@ Alternatively, you can use [pgloader](https://github.com/dimitri/pgloader), a fl
Install pgloader.
Create a configuration file (e.g., config.load).
For your destination, use your Supabase connection string with `Use connection pooling` enabled, and the mode set to `Session`. You can get the string from your [`Database Settings`](https://supabase.com/dashboard/project/_/settings/general).
```sql
LOAD DATABASE
FROM mssql://USER:PASSWORD@HOST/SOURCE_DB
INTO postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres
INTO postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres
ALTER SCHEMA 'public' OWNER TO 'postgres';
set wal_buffers = '64MB', max_wal_senders = 0, statement_timeout = 0, work_mem to '2GB';
```
@@ -56,10 +56,12 @@ Alternatively, you can use [pgloader](https://github.com/dimitri/pgloader), a fl
Install pgloader.
Create a configuration file (e.g., config.load).
For your destination, use your Supabase connection string with `Use connection pooling` enabled, and the mode set to `Session`. You can get the string from your [`Database Settings`](https://supabase.com/dashboard/project/_/settings/general).
```sql
load database
from mysql://user:password@host/source_db
into postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres
into postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres
alter schema 'public' owner to 'postgres';
set wal_buffers = '64MB', max_wal_senders = 0, statement_timeout = 0, work_mem to '2GB';
```
@@ -21,13 +21,13 @@ Example:
%env PSQL_COMMAND=PGPASSWORD=RgaMDfTS_password_FTPa7 psql -h dpg-a_server_in.oregon-postgres.provider.com -U my_db_pxl0_user my_db_pxl0
```
## Retrieve your Supabase host [#retrieve-supabase-host]
## Retrieve your Supabase connection string [#retrieve-supabase-connection-string]
1. If you're new to Supabase, [create a project](https://supabase.com/dashboard).
Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database).
1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard.
1. Under **Connection Info**, note your Host (`$SUPABASE_HOST`).
1. Under **Connection string**, make sure `Use connection pooling` is enabled. Copy the URI and replace the password placeholder with your database password.
![Finding Supabase host address](/docs/img/guides/resources/migrating-to-supabase/postgres/database-settings-host.png)
@@ -66,7 +66,7 @@ pg_dump --clean --if-exists --quote-all-identifiers \
Use `psql` to import the Postgres database file to your Supabase project.
```bash
psql -h $SUPA_URL -U postgres --file dump.sql -p 6543 -d postgres
psql -d "$YOUR_CONNECTION_STRING" -f dump.sql
```
### Additional options
@@ -25,15 +25,13 @@ Example:
%env PSQL_COMMAND=PGPASSWORD=RgaMDfTS_password_FTPa7 psql -h dpg-a_server_in.oregon-postgres.render.com -U my_db_pxl0_user my_db_pxl0
```
## Retrieve your Supabase host [#retrieve-supabase-host]
## Retrieve your Supabase connection string [#retrieve-supabase-connection-string]
1. If you're new to Supabase, [create a project](https://supabase.com/dashboard).
Make a note of your password, you will need this later. If you forget it, you can [reset it here](https://supabase.com/dashboard/project/_/settings/database).
1. Go to the [Database settings](https://supabase.com/dashboard/project/_/settings/database) for your project in the Supabase Dashboard.
1. Under **Connection Info**, note your Host (`$SUPABASE_HOST`).
![Finding Supabase host address](/docs/img/guides/resources/migrating-to-supabase/render/database-settings-host.png)
1. Under **Connection string**, make sure `Use connection pooling` is enabled. Copy the URI and replace the password placeholder with your database password.
## Migrate the database
@@ -70,7 +68,7 @@ pg_dump --clean --if-exists --quote-all-identifiers \
Use `psql` to import the Render database file to your Supabase project.
```bash
psql -h $SUPA_URL -U postgres --file render_dump.sql -p 6543 -d postgres
psql -d "$YOUR_CONNECTION_STRING" -f render_dump.sql
```
### Additional options
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

+8 -2
View File
@@ -12,7 +12,10 @@ export type Section = {
export abstract class BaseLoader {
type: string
constructor(public source: string, public path: string) {}
constructor(
public source: string,
public path: string
) {}
abstract load(): Promise<BaseSource[]>
}
@@ -23,7 +26,10 @@ export abstract class BaseSource {
meta?: Json
sections?: Section[]
constructor(public source: string, public path: string) {}
constructor(
public source: string,
public path: string
) {}
abstract process(): { checksum: string; meta?: Json; ragIgnore?: boolean; sections: Section[] }
@@ -77,7 +77,10 @@ export async function fetchDiscussions(owner: string, repo: string, categoryId:
export class GitHubDiscussionLoader extends BaseLoader {
type = 'github-discussions' as const
constructor(source: string, public discussion: Discussion) {
constructor(
source: string,
public discussion: Discussion
) {
super(source, discussion.url)
}
@@ -89,7 +92,11 @@ export class GitHubDiscussionLoader extends BaseLoader {
export class GitHubDiscussionSource extends BaseSource {
type = 'github-discussions' as const
constructor(source: string, path: string, public discussion: Discussion) {
constructor(
source: string,
path: string,
public discussion: Discussion
) {
super(source, path)
}
+9 -2
View File
@@ -192,7 +192,10 @@ export type ProcessedMdx = {
export class MarkdownLoader extends BaseLoader {
type = 'markdown' as const
constructor(source: string, public filePath: string) {
constructor(
source: string,
public filePath: string
) {
const path = filePath.replace(/^pages/, '').replace(/\.mdx?$/, '')
super(source, path)
}
@@ -206,7 +209,11 @@ export class MarkdownLoader extends BaseLoader {
export class MarkdownSource extends BaseSource {
type = 'markdown' as const
constructor(source: string, path: string, public contents: string) {
constructor(
source: string,
path: string,
public contents: string
) {
super(source, path)
}
@@ -34,7 +34,10 @@ export async function fetchPartners() {
export class IntegrationLoader extends BaseLoader {
type = 'partner-integration' as const
constructor(source: string, public partnerData: PartnerData) {
constructor(
source: string,
public partnerData: PartnerData
) {
const relPath = `/partners/integrations/${partnerData.slug}`
super(source, relPath)
}
@@ -47,7 +50,11 @@ export class IntegrationLoader extends BaseLoader {
export class IntegrationSource extends BaseSource {
type = 'partner-integration' as const
constructor(source: string, path: string, public partnerData: PartnerData) {
constructor(
source: string,
path: string,
public partnerData: PartnerData
) {
super(source, path)
}
+3 -3
View File
@@ -7,9 +7,9 @@
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"~/*": ["./*"]
}
"~/*": ["./*"],
},
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
}
+1 -1
View File
@@ -25,7 +25,7 @@
"@types/lodash": "^4.14.202",
"json-stringify-safe": "^5.0.1",
"lodash": "^4.17.21",
"prettier": "^2.8.8",
"prettier": "^4.0.0-alpha.8",
"tsx": "^4.7.0"
},
"prettier": {
+48 -17
View File
@@ -3502,6 +3502,7 @@ functions:
- Realtime is disabled by default for new Projects for better database performance and security. You can turn it on by [managing replication](/docs/guides/database/api#managing-realtime).
- If you want to receive the "previous" data for updates and deletes, you will need to set `REPLICA IDENTITY` to `FULL`, like this: `ALTER TABLE your_table REPLICA IDENTITY FULL;`
- When using a method with a generic type like `track`, `broadcast` or `broadcastFlow`, you have to provide a [serializable class](/docs/reference/kotlin/installing#serialization) as the type parameter.
- Presence, Broadcast and Database updates are sent through a [Flow](https://kotlinlang.org/docs/flow.html)
examples:
- id: liste-to-broadcasts
name: Listen to broadcasts
@@ -3516,7 +3517,7 @@ functions:
val broadcastFlow = channel.broadcastFlow<Message>(event = "message")
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
//Collect the flow
broadcastFlow.collect { //it: Message
println(it)
}
@@ -3532,22 +3533,27 @@ functions:
@Serializable
data class PresenceState(val username: String)
val connectedUsers = mutableSetOf<PresenceState>()
val channel = supabase.channel("channelId") {
//optional config
}
//if you want more control and want to receive the raw data, use the `presenceChangeFlow` method
val presenceFlow = channel.presenceDataFlow<PresenceState>()
val presenceChangeFlow = channel.presenceChangeFlow()
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
presenceChangeFlow.collect {
connectedUsers += it.decodeJoinsAs<PresenceState>()
connectedUsers -= it.decodeLeavesAs<PresenceState>()
//Collect the flow
presenceFlow.collect {
for (presence in it) {
println(presence.username)
}
}
channel.subscribe(blockUntilSubscribed = true)
//send own state
//send your own state
channel.track(PresenceState(username = "John"))
description: |
You have two methods you can use to listen for presence updates:
- `presenceDataFlow` takes care of caching presences based on their id and automatically emits the updated list.
- `presenceChangeFlow` emits the raw data received by realtime. Can be used if more control is required.
- id: listen-to-all-database-changes
name: Listen to all database changes
code: |
@@ -3557,7 +3563,7 @@ functions:
}
val changeFlow = channel.postgresChangeFlow<PostgresAction>(schema = "public")
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
//Collect the flow
changeFlow.collect {
when(it) {
is PostgresAction.Delete -> println("Deleted: ${it.oldRecord}")
@@ -3576,11 +3582,17 @@ functions:
val channel = supabase.channel("channelId") {
//optional config
}
//This flow will automatically emit the initial data of all messages (which can be restricted by specifying a filter) and then listens for updates.
//The data is cached automatically by the primary key you specify
val users: Flow<List<User>> = channel.postgresListDataFlow(schema = "public", table = "users", primaryKey = User::id)
//If you want more control and want to receive the raw data use the `postgresChangeFlow` method:
val changeFlow = channel.postgresChangeFlow<PostgresAction>(schema = "public") {
table = "users"
}
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
//Collect the flow
changeFlow.collect {
when(it) {
is PostgresAction.Delete -> println("Deleted: ${it.oldRecord}")
@@ -3592,6 +3604,10 @@ functions:
channel.subscribe()
```
description: |
You have two methods you can use if you want to listen for database updates on a specific table:
- `postgresListDataFlow` fetches the initial data of all database entries matching your filter (or all entries if you didn't specify one) and then caches the data based on their primary key. Inserts, Updates and Deletes are handled automatically and you will get the updated list through the flow.
- `postgresChangeFlow` emits the raw data received by realtime. Events can be filtered by specifying a `PostgresAction` type parameter (see examples), or all by specifying only `PostgresAction`. Can be used if more control is required.
- id: listen-to-inserts
name: Listen to inserts
code: |
@@ -3603,7 +3619,7 @@ functions:
table = "users"
}
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
//Collect the flow
changeFlow.collect {
println(it.record)
}
@@ -3624,11 +3640,12 @@ functions:
val channel = supabase.channel("channelId") {
//optional config
}
val changeFlow = channel.postgresChangeFlow<PostgresAction.Update>(schema = "public") {
table = "users"
}
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
//Collect the flow
changeFlow.collect {
println(it.record)
println(it.oldRecord)
@@ -3650,11 +3667,12 @@ functions:
val channel = supabase.channel("channelId") {
//optional config
}
val changeFlow = channel.postgresChangeFlow<PostgresAction.Delete>(schema = "public") {
table = "users"
}
//in a new coroutine (or use Flow.onEach().launchIn(scope)):
//Collect the flow
changeFlow.collect {
println(it.oldRecord)
}
@@ -3663,24 +3681,35 @@ functions:
```
- id: listening-to-row-level-changes
name: Listen to row level changes
description: You can listen to individual rows using the format `{table}:{col}=eq.{val}` - where `{col}` is the column name, and `{val}` is the value which you want to match.
code: |
```kotlin
val channel = supabase.channel("channelId") {
//optional config
}
//This flow will automatically emit the user's initial data and then emits new values on update. The flow ends on deletion
val user: Flow<User> = channel.postgresSingleDataFlow(schema = "public", table = "users", primaryKey = User::id) {
//This is the same filter as used in postgrest, so you could use complex queries, but only one entry is fetched
eq("id", 1)
}
//If you want more control, use the `postgresChangeFlow` method which works the same as the other examples
val changeFlow = channel.postgresChangeFlow<PostgresAction.Delete>(schema = "public") {
table = "users"
filter = "id=eq.1"
filter("id", FilterOperator.EQ, 1)
}
//in a new coroutine:
//Collect the flow
changeFlow.collect {
println(it.oldRecord)
}
channel.subscribe()
```
description: |
You have two methods you can use if you want to listen for updates on a specific database row:
- `postgresSingleDataFlow` fetches the initial data matching the filter and then emits updates based on realtime events.
- `postgresChangeFlow` emits the raw data received by realtime. To filter the realtime queries, use the `filter` method.
- id: remove-channel
description: |
@@ -3689,6 +3718,7 @@ functions:
notes: |
- Removing a channel is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes.
- Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed.
- If you removed all channels, the client automatically disconnects from the Realtime websocket. This can be disabled in the Realtime config by setting `disconnectOnNoSubscriptions` to false.
examples:
- id: removes-a-channel
name: Remove a channel
@@ -3718,6 +3748,7 @@ functions:
notes: |
Unsubscribes and removes all Realtime channels from Realtime client.
- Removing channels is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes. Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed.
- If you removed all channels, the client automatically disconnects from the Realtime websocket. This can be disabled in the Realtime config by setting `disconnectOnNoSubscriptions` to false.
examples:
- id: remove-all-channels
name: Remove all channels
+3 -1
View File
@@ -91,7 +91,9 @@
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
transition:
color 0.15s ease,
border-color 0.15s ease;
max-width: 300px;
}
+4 -4
View File
@@ -5,7 +5,7 @@
"baseUrl": ".",
"paths": {
"~/*": ["./*"],
"@ui/*": ["./../../packages/ui/src/*"] // handle ui package paths
"@ui/*": ["./../../packages/ui/src/*"], // handle ui package paths
},
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext"],
@@ -19,14 +19,14 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"module": "esnext"
"module": "esnext",
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"pages/guides/append-test.js",
"jest.config.mjs"
"jest.config.mjs",
],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
}
+3 -3
View File
@@ -9,8 +9,8 @@ type XOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T
type OneOf<T extends any[]> = T extends [infer Only]
? Only
: T extends [infer A, infer B, ...infer Rest]
? OneOf<[XOR<A, B>, ...Rest]>
: never
? OneOf<[XOR<A, B>, ...Rest]>
: never
export interface paths {
'/platform/notifications': {
@@ -3846,7 +3846,7 @@ export interface components {
message?: string
status?: string
},
string
string,
]
>
result?: Record<string, never>[]
@@ -62,12 +62,12 @@ export function useKeyboardShortcuts(
return event.metaKey && event.shiftKey
? `Command+Shift+${event.key}`
: event.metaKey
? `Command+${event.key}`
: event.shiftKey && event.key === 'Enter'
? `Shift+${event.key}`
: event.ctrlKey && event.key
? `Control+${event.key}`
: event.key
? `Command+${event.key}`
: event.shiftKey && event.key === 'Enter'
? `Shift+${event.key}`
: event.ctrlKey && event.key
? `Control+${event.key}`
: event.key
}
React.useEffect(() => {
@@ -18,8 +18,8 @@ export function Shortcuts({ gridRef }: ShortcutsProps) {
return navigator?.appVersion.indexOf('Win') !== -1
? 'windows'
: navigator?.appVersion.indexOf('Mac') !== -1
? 'macos'
: 'unknown'
? 'macos'
: 'unknown'
}
const metakey = getClientOS() === 'windows' ? 'Control' : 'Command'
setMetaKey(metakey)
@@ -370,8 +370,8 @@ const RowHeader = ({ table, sorts, filters }: RowHeaderProps) => {
{allRowsSelected
? `${totalRows} rows selected`
: selectedRows.size > 1
? `${selectedRows.size} rows selected`
: `${selectedRows.size} row selected`}
? `${selectedRows.size} rows selected`
: `${selectedRows.size} row selected`}
</span>
{!allRowsSelected && totalRows > allRows.length && (
<Button type="link" onClick={() => onSelectAllRows()}>
@@ -404,8 +404,8 @@ const RowHeader = ({ table, sorts, filters }: RowHeaderProps) => {
{allRowsSelected
? `Delete ${totalRows} rows`
: selectedRows.size > 1
? `Delete ${selectedRows.size} rows`
: `Delete ${selectedRows.size} row`}
? `Delete ${selectedRows.size} rows`
: `Delete ${selectedRows.size} row`}
</Button>
</Tooltip.Trigger>
@@ -25,8 +25,8 @@ const FilterRow = ({ table, filter, filterIdx, onChange, onDelete, onKeyDown }:
column?.format === 'timestamptz'
? 'yyyy-mm-dd hh:mm:ss+zz'
: column?.format === 'timestamp'
? 'yyyy-mm-dd hh:mm:ss'
: 'Enter a value'
? 'yyyy-mm-dd hh:mm:ss'
: 'Enter a value'
return (
<div className="sb-grid-filter-row px-3">
@@ -56,8 +56,8 @@ export function getGridColumns(
const columnWidth = options?.defaultWidth
? options.defaultWidth
: columnDefaultWidth < columnWidthBasedOnName
? columnWidthBasedOnName
: columnDefaultWidth
? columnWidthBasedOnName
: columnDefaultWidth
const columnDefinition: CalculatedColumn<SupaRow> = {
key: x.name,
@@ -247,8 +247,8 @@ const AuditLogs = () => {
{project?.name
? 'Project: '
: organization?.name
? 'Organization: '
: null}
? 'Organization: '
: null}
{project?.name ?? organization?.name ?? '-'}
</p>
<p
@@ -258,8 +258,8 @@ const AuditLogs = () => {
{log.target.metadata.project_ref
? 'Ref: '
: log.target.metadata.org_slug
? 'Slug: '
: null}
? 'Slug: '
: null}
{log.target.metadata.project_ref ?? log.target.metadata.org_slug}
</p>
</Table.td>
@@ -3,38 +3,75 @@ import { PropsWithChildren, createContext, useContext, useEffect, useState } fro
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
const NOTICE_BANNER_KEY = LOCAL_STORAGE_KEYS.PGBOUNCER_DEPRECATION_WARNING
const PGBOUNCER_BANNER_KEY = LOCAL_STORAGE_KEYS.PGBOUNCER_IPV6_DEPRECATION_WARNING
const VERCEL_BANNER_KEY = LOCAL_STORAGE_KEYS.VERCEL_IPV6_DEPRECATION_WARNING
const DEFAULT_NOTICE_BANNER_KEY = LOCAL_STORAGE_KEYS.PGBOUNCER_DEPRECATION_WARNING
// [Joshen] Update this as and when we need to use the NoticeBanner
type AppBannerContextType = {
acknowledged: boolean
onUpdateAcknowledged: (value: boolean) => void
ipv6BannerAcknowledged: boolean
pgbouncerBannerAcknowledged: string[]
vercelBannerAcknowledged: string[]
onUpdateAcknowledged: (key: 'ipv6' | 'pgbouncer' | 'vercel', value: boolean | string) => void
}
const AppBannerContext = createContext<AppBannerContextType>({
acknowledged: false,
ipv6BannerAcknowledged: false,
pgbouncerBannerAcknowledged: [],
vercelBannerAcknowledged: [],
onUpdateAcknowledged: noop,
})
export const useAppBannerContext = () => useContext(AppBannerContext)
export const AppBannerContextProvider = ({ children }: PropsWithChildren<{}>) => {
const [acknowledged, setAcknowledged] = useState(false)
const [ipv6BannerAcknowledged, setIpv6BannerAcknowledged] = useState(false)
// [Joshen] These will take a list of refs instead since they are project specific
// Comma separated strings, no spaces
const [pgbouncerBannerAcknowledged, setPgbouncerBannerAcknowledged] = useState<string[]>([])
const [vercelBannerAcknowledged, setVercelBannerAcknowledged] = useState<string[]>([])
useEffect(() => {
if (typeof window !== 'undefined') {
setAcknowledged(localStorage.getItem(NOTICE_BANNER_KEY) === 'true')
setIpv6BannerAcknowledged(localStorage.getItem(DEFAULT_NOTICE_BANNER_KEY) === 'true')
const acknowledgedPbBouncerRefs = localStorage.getItem(PGBOUNCER_BANNER_KEY)?.split(',') ?? []
setPgbouncerBannerAcknowledged(acknowledgedPbBouncerRefs)
const acknowledgedVercelRefs = localStorage.getItem(VERCEL_BANNER_KEY)?.split(',') ?? []
setVercelBannerAcknowledged(acknowledgedVercelRefs)
}
}, [])
const value = {
acknowledged,
onUpdateAcknowledged: (value: boolean) => {
if (typeof window !== 'undefined') {
window.localStorage.setItem(NOTICE_BANNER_KEY, value.toString())
ipv6BannerAcknowledged,
pgbouncerBannerAcknowledged,
vercelBannerAcknowledged,
onUpdateAcknowledged: (key: 'ipv6' | 'pgbouncer' | 'vercel', value: boolean | string) => {
if (key === 'ipv6' && typeof value === 'boolean') {
if (typeof window !== 'undefined') {
window.localStorage.setItem(DEFAULT_NOTICE_BANNER_KEY, value.toString())
}
setIpv6BannerAcknowledged(value)
}
if (key === 'pgbouncer' && typeof value === 'string' && value.length > 0) {
const updatedRefs = pgbouncerBannerAcknowledged.concat([value])
if (typeof window !== 'undefined') {
window.localStorage.setItem(PGBOUNCER_BANNER_KEY, updatedRefs.join(','))
}
setPgbouncerBannerAcknowledged(updatedRefs)
}
if (key === 'vercel' && typeof value === 'string' && value.length > 0) {
const updatedRefs = pgbouncerBannerAcknowledged.concat([value])
if (typeof window !== 'undefined') {
window.localStorage.setItem(VERCEL_BANNER_KEY, updatedRefs.join(','))
}
setVercelBannerAcknowledged(updatedRefs)
}
setAcknowledged(value)
},
}
@@ -42,6 +79,7 @@ export const AppBannerContextProvider = ({ children }: PropsWithChildren<{}>) =>
}
export const useIsNoticeBannerShown = () => {
const { acknowledged } = useAppBannerContext()
return acknowledged
const { ipv6BannerAcknowledged, pgbouncerBannerAcknowledged, vercelBannerAcknowledged } =
useAppBannerContext()
return ipv6BannerAcknowledged && pgbouncerBannerAcknowledged && vercelBannerAcknowledged
}
@@ -18,7 +18,7 @@ const CLSPreview = () => {
className="text-foreground-light max-w-full"
content={`This is an advanced feature and should be used with caution. Unless you have a very specific use case, we recommend just using [Row-Level Security](https://supabase.com/docs/guides/auth/row-level-security).`}
/>
<Alert_Shadcn_ variant="warning" className="mt-8">
<Alert_Shadcn_ variant="warning" className="mt-2">
<IconAlertCircle strokeWidth={2} />
<AlertTitle_Shadcn_>
Changes to column privileges will not be reflected in migrations when running{' '}
@@ -2,7 +2,7 @@ import { useTelemetryProps } from 'common'
import { FlaskConical } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Button, IconExternalLink, IconEye, IconEyeOff, Modal, ScrollArea, cn } from 'ui'
import { useFlag } from 'hooks'
@@ -47,7 +47,19 @@ const FeaturePreviewModal = () => {
const snap = useAppStateSnapshot()
const telemetryProps = useTelemetryProps()
const featurePreviewContext = useFeaturePreviewContext()
const [selectedFeatureKey, setSelectedFeatureKey] = useState<string>(FEATURE_PREVIEWS?.[0]?.key)
const selectedFeaturePreview =
snap.selectedFeaturePreview === '' ? FEATURE_PREVIEWS[0].key : snap.selectedFeaturePreview
const [selectedFeatureKey, setSelectedFeatureKey] = useState<string>(selectedFeaturePreview)
// this modal can be triggered on other pages
// Update local state when valtio state changes
useEffect(() => {
if (snap.selectedFeaturePreview !== '') {
setSelectedFeatureKey(snap.selectedFeaturePreview)
}
}, [snap.selectedFeaturePreview])
const { flags, onUpdateFlag } = featurePreviewContext
const selectedFeature = FEATURE_PREVIEWS.find((preview) => preview.key === selectedFeatureKey)
@@ -66,6 +78,11 @@ const FeaturePreviewModal = () => {
)
}
function handleCloseFeaturePreviewModal() {
snap.setShowFeaturePreviewModal(false)
snap.setSelectedFeaturePreview(FEATURE_PREVIEWS[0].key)
}
return (
<Modal
hideFooter
@@ -74,12 +91,12 @@ const FeaturePreviewModal = () => {
className="max-w-4xl"
header="Dashboard feature previews"
visible={snap.showFeaturePreviewModal}
onCancel={() => snap.setShowFeaturePreviewModal(false)}
onCancel={handleCloseFeaturePreviewModal}
>
{FEATURE_PREVIEWS.length > 0 ? (
<div className="flex border-t">
<div>
<ScrollArea className="h-[550px] w-[240px] border-r">
<ScrollArea className="h-[550px] w-[280px] border-r">
{FEATURE_PREVIEWS.map((feature) => {
const isEnabled = flags[feature.key] ?? false
@@ -97,7 +114,9 @@ const FeaturePreviewModal = () => {
) : (
<IconEyeOff size={14} strokeWidth={1.5} className="text-foreground-light" />
)}
<p className="text-sm">{feature.name}</p>
<p className="text-sm truncate" title={feature.name}>
{feature.name}
</p>
</div>
)
})}
@@ -59,8 +59,8 @@ const ProviderForm = ({ config, provider }: ProviderFormProps) => {
initialValues[key] = configValue
? configValue
: provider.properties[key].type === 'boolean'
? false
: ''
? false
: ''
}
}
})
@@ -52,7 +52,7 @@ export const AIPolicyPre = ({ onDiff, children, className }: AAIPolicyPreProps)
hideCopy
hideLineNumbers
/>
<div className="absolute top-3 right-3 bg-surface-100 border-muted border rounded-lg h-[28px] hidden group-hover:block">
<div className="absolute top-5 right-2 bg-surface-100 border-muted border rounded-lg h-[28px] hidden group-hover:block">
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger asChild>
<Button
@@ -33,8 +33,8 @@ const PolicyDetails = ({ policy, showDetails, toggleShowDetails }: PolicyDetails
/>
</div>
</CollapsibleTrigger_Shadcn_>
<CollapsibleContent_Shadcn_ className="pl-[3.6rem] my-2 grid gap-1.5">
<div className="flex">
<CollapsibleContent_Shadcn_ className="pl-[3.6rem] grid gap-1.5">
<div className="flex my-2">
<div>
<div className="text-xs flex items-start space-x-2 border-b py-1.5">
<p className="w-[110px] text-foreground-light">Name:</p>
@@ -19,7 +19,7 @@ import { databasePoliciesKeys } from 'data/database-policies/keys'
import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query'
import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { useSelectedOrganization, useSelectedProject } from 'hooks'
import { BASE_PATH, OPT_IN_TAGS } from 'lib/constants'
import { BASE_PATH, LOCAL_STORAGE_KEYS, OPT_IN_TAGS } from 'lib/constants'
import { uuidv4 } from 'lib/helpers'
import { AIPolicyChat } from './AIPolicyChat'
import {
@@ -35,6 +35,7 @@ import RLSCodeEditor from './RLSCodeEditor'
import Telemetry from 'lib/telemetry'
import { useTelemetryProps } from 'common'
import { useRouter } from 'next/router'
import { useAppStateSnapshot } from 'state/app-state'
const DiffEditor = dynamic(
() => import('@monaco-editor/react').then(({ DiffEditor }) => DiffEditor),
@@ -60,6 +61,7 @@ export const AIPolicyEditorPanel = memo(function ({
const selectedProject = useSelectedProject()
const selectedOrganization = useSelectedOrganization()
const router = useRouter()
const snap = useAppStateSnapshot()
const telemetryProps = useTelemetryProps()
// use chat id because useChat doesn't have a reset function to clear all messages
@@ -175,6 +177,12 @@ export const AIPolicyEditorPanel = memo(function ({
setIncomingChange(undefined)
}, [incomingChange])
function toggleFeaturePreviewModal() {
snap.setSelectedFeaturePreview(LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_AI_ASSISTANT)
snap.setShowFeaturePreviewModal(!snap.showFeaturePreviewModal)
onSelectCancel()
}
const onClosingPanel = useCallback(() => {
const policy = editorRef.current?.getValue()
if (policy || messages.length > 0 || !isAssistantChatInputEmpty) {
@@ -347,8 +355,11 @@ export const AIPolicyEditorPanel = memo(function ({
setOpen={setErrorPanelOpen}
/>
)}
<SheetFooter_Shadcn_ className="flex flex-col gap-12 px-5 py-4 w-full">
<div className="flex justify-end gap-x-2">
<SheetFooter_Shadcn_ className="flex items-center !justify-between px-5 py-4 w-full">
<Button type="text" onClick={toggleFeaturePreviewModal}>
Toggle feature preview
</Button>
<div className="flex items-center gap-x-2">
<Button type="default" disabled={isExecuting} onClick={() => onSelectCancel()}>
Cancel
</Button>
@@ -217,6 +217,7 @@ const Policies = ({
)}
<PolicyEditorModal
showAssistantPreview
visible={!isEmpty(selectedSchemaAndTable)}
schema={selectedSchemaAndTable.schema}
table={selectedSchemaAndTable.table}
@@ -22,8 +22,8 @@ export const createSQLPolicy = (
definition: definition
? definition.replace(/\s+/g, ' ').trim()
: definition === undefined
? null
: definition,
? null
: definition,
check: check ? check.replace(/\s+/g, ' ').trim() : check === undefined ? null : check,
}
@@ -1,5 +1,5 @@
import { noop } from 'lodash'
import { Button, IconExternalLink } from 'ui'
import { Button } from 'ui'
interface PolicyEditorFooterProps {
showTemplates: boolean
@@ -13,15 +13,6 @@ const PolicyEditorFooter = ({
onReviewPolicy = noop,
}: PolicyEditorFooterProps) => (
<div className="flex justify-between items-center border-t px-6 py-4 border-default">
<a
href="https://supabase.com/docs/learn/auth-deep-dive/auth-policies"
target="_blank"
rel="noreferrer"
>
<Button type="link" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
Documentation
</Button>
</a>
<div className="flex w-full items-center justify-end gap-2">
{showTemplates && (
<Button type="default" onClick={onViewTemplates}>
@@ -1,14 +1,18 @@
import { noop } from 'lodash'
import { IconChevronLeft } from 'ui'
import { Button, IconChevronLeft, IconExternalLink } from 'ui'
import { POLICY_MODAL_VIEWS } from 'components/interfaces/Auth/Policies'
import { FlaskConical } from 'lucide-react'
import { useAppStateSnapshot } from 'state/app-state'
interface PolicyEditorModalTitleProps {
view: string
schema: string
table: string
isNewPolicy: boolean
showAssistantPreview: boolean
onSelectBackFromTemplates: () => void
onToggleFeaturePreviewModal: () => void
}
const PolicyEditorModalTitle = ({
@@ -16,8 +20,12 @@ const PolicyEditorModalTitle = ({
schema,
table,
isNewPolicy,
showAssistantPreview,
onSelectBackFromTemplates = noop,
onToggleFeaturePreviewModal,
}: PolicyEditorModalTitleProps) => {
const snap = useAppStateSnapshot()
const getTitle = () => {
if (view === POLICY_MODAL_VIEWS.EDITOR || view === POLICY_MODAL_VIEWS.SELECTION) {
return `${isNewPolicy ? 'Adding new policy to' : 'Editing policy from'} ${schema}.${table}`
@@ -26,6 +34,7 @@ const PolicyEditorModalTitle = ({
return `Reviewing policy to be ${isNewPolicy ? 'created' : 'updated'} on ${schema}.${table}`
}
}
if (view === POLICY_MODAL_VIEWS.TEMPLATES) {
return (
<div>
@@ -36,14 +45,37 @@ const PolicyEditorModalTitle = ({
>
<IconChevronLeft strokeWidth={2} size={14} />
</span>
<h4 className="m-0 text-lg">Select a template to use for your new policy</h4>
<h4>Select a template to use for your new policy</h4>
</div>
</div>
)
}
return (
<div className="flex items-center space-x-3">
<h4 className="m-0 truncate text-lg">{getTitle()}</h4>
<div className="w-full flex items-center justify-between gap-x-4">
<h4 className="truncate" title={getTitle()}>
{getTitle()}
</h4>
<div className="flex items-center gap-x-2">
{showAssistantPreview && view === POLICY_MODAL_VIEWS.EDITOR && (
<Button
type="default"
icon={<FlaskConical size={14} />}
onClick={onToggleFeaturePreviewModal}
>
Try Supabase Assistant
</Button>
)}
<Button asChild type="default" icon={<IconExternalLink size={14} />}>
<a
href="https://supabase.com/docs/learn/auth-deep-dive/auth-policies"
target="_blank"
rel="noreferrer"
>
{' '}
Documentation
</a>
</Button>
</div>
</div>
)
}
@@ -4,6 +4,8 @@ import { Modal } from 'ui'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { useStore } from 'hooks'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { POLICY_MODAL_VIEWS } from '../Policies.constants'
import {
PolicyFormField,
@@ -28,6 +30,7 @@ interface PolicyEditorModalProps {
schema: string
table: string
selectedPolicyToEdit: any
showAssistantPreview?: boolean
onSelectCancel: () => void
onCreatePolicy: (payload: PostgresPolicyCreatePayload) => Promise<boolean>
onUpdatePolicy: (payload: PostgresPolicyUpdatePayload) => Promise<boolean>
@@ -39,12 +42,14 @@ const PolicyEditorModal = ({
schema = '',
table = '',
selectedPolicyToEdit = {},
showAssistantPreview = false,
onSelectCancel = noop,
onCreatePolicy,
onUpdatePolicy,
onSaveSuccess = noop,
}: PolicyEditorModalProps) => {
const { ui } = useStore()
const snap = useAppStateSnapshot()
const newPolicyTemplate: PolicyFormField = {
schema,
@@ -91,6 +96,12 @@ const PolicyEditorModal = ({
const onReviewPolicy = () => setView(POLICY_MODAL_VIEWS.REVIEW)
const onSelectBackFromTemplates = () => setView(previousView)
const onToggleFeaturePreviewModal = () => {
snap.setSelectedFeaturePreview(LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_AI_ASSISTANT)
snap.setShowFeaturePreviewModal(!snap.showFeaturePreviewModal)
onSelectCancel()
}
const onUseTemplate = (template: PolicyTemplate) => {
setPolicyFormFields({
...policyFormFields,
@@ -171,9 +182,8 @@ const PolicyEditorModal = ({
return (
<Modal
size={view === POLICY_MODAL_VIEWS.SELECTION ? 'medium' : 'xxlarge'}
closable
hideFooter
size={view === POLICY_MODAL_VIEWS.SELECTION ? 'medium' : 'xxlarge'}
visible={visible}
contentStyle={{ padding: 0 }}
header={[
@@ -183,7 +193,9 @@ const PolicyEditorModal = ({
isNewPolicy={isNewPolicy}
schema={schema}
table={table}
showAssistantPreview={showAssistantPreview}
onSelectBackFromTemplates={onSelectBackFromTemplates}
onToggleFeaturePreviewModal={onToggleFeaturePreviewModal}
/>,
]}
onCancel={isClosingPolicyEditor}
@@ -212,6 +224,8 @@ const PolicyEditorModal = ({
description="Write rules with PostgreSQL's policies to fit your unique business needs."
onViewTemplates={onViewTemplates}
onViewEditor={onViewEditor}
showAssistantPreview={showAssistantPreview}
onToggleFeaturePreviewModal={onToggleFeaturePreviewModal}
/>
) : view === POLICY_MODAL_VIEWS.EDITOR ? (
<PolicyEditor
@@ -1,25 +1,40 @@
import { noop } from 'lodash'
import { IconEdit, IconGrid, Modal } from 'ui'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
IconEdit,
IconExternalLink,
IconGrid,
Modal,
} from 'ui'
import CardButton from 'components/ui/CardButton'
import { FlaskConical } from 'lucide-react'
import { useAppStateSnapshot } from 'state/app-state'
interface PolicySelectionProps {
description: string
showAssistantPreview: boolean
onViewTemplates: () => void
onViewEditor: () => void
onToggleFeaturePreviewModal?: () => void
}
const PolicySelection = ({
description = '',
showAssistantPreview,
onViewTemplates = noop,
onViewEditor = noop,
onToggleFeaturePreviewModal,
}: PolicySelectionProps) => {
const snap = useAppStateSnapshot()
return (
<Modal.Content>
<div className="space-y-6 py-8">
<div>
<p className="text-sm text-foreground-light">{description}</p>
</div>
<Modal.Content className="space-y-4 py-4">
<div className="flex flex-col gap-y-2">
<p className="text-sm text-foreground-light">{description}</p>
<div className="grid grid-cols-1 gap-2 lg:grid-cols-1">
<CardButton
title="Get started quickly"
@@ -30,7 +45,7 @@ const PolicySelection = ({
className="
flex h-8 w-8 items-center
justify-center
rounded bg-foreground text-background
rounded bg-foreground text-background
"
>
<IconGrid size={14} strokeWidth={2} />
@@ -48,7 +63,7 @@ const PolicySelection = ({
className="
flex h-8 w-8 items-center
justify-center
rounded bg-foreground text-background
rounded bg-foreground text-background
"
>
<IconEdit size={14} strokeWidth={2} />
@@ -58,19 +73,31 @@ const PolicySelection = ({
onClick={onViewEditor}
/>
</div>
<p className="text-sm text-foreground-light">
Not sure what policies are? Check out our resources{' '}
<a
target="_blank"
rel="noreferrer"
className="text-brand transition-colors hover:text-brand-600"
href="https://supabase.com/docs/guides/auth#policies"
>
here
</a>
.
</p>
</div>
{showAssistantPreview && onToggleFeaturePreviewModal !== undefined && (
<Alert_Shadcn_>
<FlaskConical />
<AlertTitle_Shadcn_>Try the new Supabase Assistant for RLS policies</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
Create RLS policies for your tables with the help of AI
</AlertDescription_Shadcn_>
<div className="flex items-center gap-x-2 mt-3">
<Button type="default" onClick={onToggleFeaturePreviewModal}>
Toggle feature preview
</Button>
<Button asChild type="default" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
<a
href="https://supabase.com/blog/studio-introducing-assistant#introducing-the-supabase-assistant"
target="_blank"
rel="noreferrer"
>
Learn more
</a>
</Button>
</div>
</Alert_Shadcn_>
)}
</Modal.Content>
)
}
@@ -10,11 +10,10 @@ export const getAddons = (selectedAddons: ProjectSelectedAddon[]) => {
return { computeInstance, pitr, customDomain, ipv4 }
}
export const subscriptionHasHipaaAddon = (subscription: OrgSubscription | undefined): boolean => {
export const subscriptionHasHipaaAddon = (subscription?: OrgSubscription): boolean => {
if (!IS_PLATFORM) return false
return (
subscription !== undefined &&
subscription.addons.some((addon) => addon.supabase_prod_id === 'addon_security_hipaa')
return (subscription?.addons ?? []).some(
(addon) => addon.supabase_prod_id === 'addon_security_hipaa'
)
}
@@ -34,6 +34,7 @@ import {
PullRequestsEmptyState,
} from './EmptyStates'
import Overview from './Overview'
import { MessageCircle } from 'lucide-react'
const BranchManagement = () => {
const { ui } = useStore()
@@ -164,44 +165,45 @@ const BranchManagement = () => {
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex items-center">
<Button
type="default"
className={`rounded-r-none transition hover:opacity-90 ${
view === 'overview' ? 'opacity-100' : 'opacity-60'
}`}
onClick={() => setView('overview')}
>
Overview
</Button>
<Button
type="default"
className={`rounded-none transition hover:opacity-90 ${
view === 'prs' ? 'opacity-100' : 'opacity-60'
}`}
onClick={() => setView('prs')}
>
Pull requests
</Button>
<Button
type="default"
className={`rounded-l-none transition hover:opacity-90 ${
view === 'branches' ? 'opacity-100' : 'opacity-60'
}`}
onClick={() => setView('branches')}
>
All branches
</Button>
</div>
<Input
size="tiny"
className="w-64"
placeholder="Search"
icon={<IconSearch size={14} strokeWidth={2} />}
/>
<div className="flex items-center -space-x-px">
<Button
type="default"
className={`rounded-r-none transition hover:opacity-90 ${
view === 'overview' ? 'opacity-100' : 'opacity-60'
}`}
onClick={() => setView('overview')}
>
Overview
</Button>
<Button
type="default"
className={`rounded-none transition hover:opacity-90 ${
view === 'prs' ? 'opacity-100' : 'opacity-60'
}`}
onClick={() => setView('prs')}
>
Pull requests
</Button>
<Button
type="default"
className={`rounded-l-none transition hover:opacity-90 ${
view === 'branches' ? 'opacity-100' : 'opacity-60'
}`}
onClick={() => setView('branches')}
>
All branches
</Button>
</div>
<div className="flex items-center justify-between space-x-2">
<Button
type={'text'}
icon={<MessageCircle className="text-muted" size={14} strokeWidth={1} />}
asChild
>
<a href="https://github.com/orgs/supabase/discussions/18937" target="_blank">
Branching Feedback
</a>
</Button>
<Button type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
<Link
target="_blank"
Loaded 100 of 385 files, more files were not shown because too many files have changed in this diff. Show more