mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 02:45:07 +03:00
Merge branch 'master' into studio-default-values
This commit is contained in:
1165 files changed
+118354
-89905
No files matched your search
@@ -17,4 +17,8 @@ jobs:
|
||||
locale: "US"
|
||||
reporter: github-pr-review
|
||||
level: error
|
||||
exclude: "*.css"
|
||||
exclude: |
|
||||
"*.css"
|
||||
"**/package.json"
|
||||
"**/package-lock.json"
|
||||
".git/*"
|
||||
@@ -121,3 +121,6 @@ typings/
|
||||
**/supabase/.temp
|
||||
|
||||
apps/new-docs/*
|
||||
|
||||
# For self-hosted logs: https://github.com/supabase/supabase/blob/86e3ab20abfdb9c3e666334d3d2f8efeef9ccf2c/docker/docker-compose-logging.yml#L101
|
||||
gcloud.json
|
||||
+1
-1
@@ -28,7 +28,7 @@ You need to install and configure the following dependencies on your machine to
|
||||
|
||||
- [Git](http://git-scm.com/)
|
||||
- [Node.js v16.x (LTS)](http://nodejs.org)
|
||||
- [npm](https://www.npmjs.com/) version 7+ or [Yarn](https://yarnpkg.com/)
|
||||
- [npm](https://www.npmjs.com/) version 8.x.x or [Yarn](https://yarnpkg.com/)
|
||||
|
||||
## Local development
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
- [x] Authentication and Authorization. [Docs](https://supabase.com/docs/guides/auth)
|
||||
- [x] Auto-generated APIs.
|
||||
- [x] REST. [Docs](https://supabase.com/docs/guides/database/api#rest-api)
|
||||
- [x] GraphQL. [Docs](https://supabase.com/docs/guides/database/api#graphql-api)
|
||||
- [x] Realtime subscriptions. [Docs](https://supabase.com/docs/guides/database/api#realtime-api)
|
||||
- [x] GraphQL (Beta). [Docs](https://supabase.com/docs/guides/database/api#graphql-api)
|
||||
- [x] Functions.
|
||||
- [x] Database Functions. [Docs](https://supabase.com/docs/guides/database/functions)
|
||||
- [x] Edge Functions [Docs](https://supabase.com/docs/guides/functions)
|
||||
@@ -63,6 +63,7 @@ You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and
|
||||
- [PostgreSQL](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.
|
||||
- [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients.
|
||||
- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API
|
||||
- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API
|
||||
- [Storage](https://github.com/supabase/storage-api) provides a RESTful interface for managing Files stored in S3, using Postgres to manage permissions.
|
||||
- [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc.
|
||||
- [GoTrue](https://github.com/netlify/gotrue) is an SWT based API for managing users and issuing SWT tokens.
|
||||
|
||||
@@ -5,32 +5,46 @@ const Favicons = () => {
|
||||
const { basePath } = useRouter()
|
||||
return (
|
||||
<Head>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href={`${basePath}/favicon/apple-touch-icon.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href={`${basePath}/favicon/favicon-32x32.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href={`${basePath}/favicon/favicon-16x16.png`}
|
||||
/>
|
||||
<link rel="manifest" href={`${basePath}/favicon/site.webmanifest`} />
|
||||
{/* <link rel="mask-icon" href={`${basePath}/favicon/safari-pinned-tab.svg`} color="#333333" /> */}
|
||||
<link rel="shortcut icon" href={`${basePath}/favicon/favicon.ico`} />
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="57x57" href={`${basePath}/favicon/apple-touch-icon-57x57.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="114x114" href={`${basePath}/favicon/apple-touch-icon-114x114.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="72x72" href={`${basePath}/favicon/apple-touch-icon-72x72.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="144x144" href={`${basePath}/favicon/apple-touch-icon-144x144.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="60x60" href={`${basePath}/favicon/apple-touch-icon-60x60.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="120x120" href={`${basePath}/favicon/apple-touch-icon-120x120.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="76x76" href={`${basePath}/favicon/apple-touch-icon-76x76.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="apple-touch-icon-precomposed" sizes="152x152" href={`${basePath}/favicon/apple-touch-icon-152x152.png`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-196x196.png" sizes="196x196`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-96x96.png" sizes="96x96`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-32x32.png" sizes="32x32`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-16x16.png" sizes="16x16`}/>
|
||||
{/* prettier-ignore */}
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon-128.png" sizes="128x128`}/>
|
||||
<meta name="application-name" content=" " />
|
||||
<meta name="msapplication-TileColor" content="#1E1E1E" />
|
||||
<meta name="msapplication-config" content={`${basePath}/favicon/browserconfig.xml`} />
|
||||
<meta name="msapplication-TileImage" content="mstile-144x144.png" />
|
||||
<meta name="msapplication-square70x70logo" content="mstile-70x70.png" />
|
||||
<meta name="msapplication-square150x150logo" content="mstile-150x150.png" />
|
||||
<meta name="msapplication-wide310x150logo" content="mstile-310x150.png" />
|
||||
<meta name="msapplication-square310x310logo" content="mstile-310x310.png" />
|
||||
<meta name="theme-color" content="#1E1E1E" />
|
||||
<link rel="alternate" type="application/rss+xml" href={`${basePath}/feed.xml`} />
|
||||
<link rel="shortcut icon" href={`${basePath}/favicon/favicon.ico`} />
|
||||
<link rel="icon" type="image/png" href={`${basePath}/favicon/favicon.ico`} />
|
||||
<link rel="apple-touch-icon" href={`${basePath}/favicon/favicon.ico`} />
|
||||
{/* misc */}
|
||||
<link rel="manifest" href={`${basePath}/favicon/site.webmanifest`} />
|
||||
<meta name="msapplication-config" content={`${basePath}/favicon/browserconfig.xml`} />
|
||||
<link rel="alternate" type="application/rss+xml" href={`${basePath}/feed.xml`} />
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import KJUR from 'jsrsasign'
|
||||
import CodeBlock from './CodeBlock/CodeBlock'
|
||||
import { Button, Select, Input } from 'ui'
|
||||
import { Button, Select, Input, CodeBlock } from 'ui'
|
||||
|
||||
const JWT_HEADER = { alg: 'HS256', typ: 'JWT' }
|
||||
const now = new Date()
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import { render } from 'react-dom'
|
||||
import { IconCommand } from 'ui'
|
||||
import { createElement, FC, useEffect, useRef, Fragment } from 'react'
|
||||
import algoliasearch from 'algoliasearch/lite'
|
||||
import { autocomplete, getAlgoliaResults } from '@algolia/autocomplete-js'
|
||||
import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
// [Joshen] We're currently using DocSearch from Algolia as it provides a nice
|
||||
// UI out of the box + some good preconfigured search settings (e.g hierarchy).
|
||||
// However, we're using our own Algolia account to store the records in the indexes
|
||||
// (rather than going through the DocSearch program from Algolia where they'll crawl
|
||||
// our site for us). Refer to scripts/build-search on how we're saving the records.
|
||||
|
||||
// Using Algolia's autocomplete library gives us full flexbility in terms of customizing
|
||||
// our search experience, but that will take time to figure out. Hence why for now we're just
|
||||
// using DocSearch with our own records.
|
||||
|
||||
// Potentially for search, we could
|
||||
// - Go ahead with the DocSearch program and let them crawl our site to generate the records
|
||||
// - But we need to ensure that our site is semantically correct first
|
||||
// - Use Algolia itself to flesh out our own search logic
|
||||
// - The basics are already set up to be honest, but will take time to make it great
|
||||
// - Go back to Typesense if we deem that Algolia is not helpful in the long run
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
|
||||
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY
|
||||
)
|
||||
|
||||
// [Joshen] Not working properly, but lets not get stuck on this
|
||||
// Priority just to get a search working
|
||||
const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
|
||||
key: 'docs-search',
|
||||
limit: 3,
|
||||
//@ts-ignore
|
||||
transformSource({ source }) {
|
||||
return {
|
||||
...source,
|
||||
templates: {
|
||||
...source.templates,
|
||||
header({ state }) {
|
||||
if (state.query) return null
|
||||
return (
|
||||
<Fragment>
|
||||
<span className="aa-SourceHeaderTitle">Your searches</span>
|
||||
<div className="aa-SourceHeaderLine" />
|
||||
</Fragment>
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
interface Props {}
|
||||
|
||||
const AlgoliaSearch: FC<Props> = ({}) => {
|
||||
const searchRef = useRef(null)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchRef.current) {
|
||||
return undefined
|
||||
}
|
||||
const search = autocomplete({
|
||||
openOnFocus: true,
|
||||
container: searchRef.current,
|
||||
defaultActiveItemId: 0,
|
||||
detachedMediaQuery: '',
|
||||
// @ts-ignore
|
||||
renderer: { createElement, Fragment, render },
|
||||
placeholder: 'Search docs',
|
||||
plugins: [recentSearchesPlugin],
|
||||
renderNoResults({ state, render }, root) {
|
||||
render(
|
||||
<div className="text-scale-1100 py-2 text-sm px-4">
|
||||
No results found for {state.query}.
|
||||
</div>,
|
||||
root
|
||||
)
|
||||
},
|
||||
navigator: {
|
||||
navigate({ itemUrl }) {
|
||||
router.push(itemUrl)
|
||||
},
|
||||
},
|
||||
onSelect({ item, setQuery, setIsOpen, refresh }) {
|
||||
//console.log('onSelect', item.url)
|
||||
},
|
||||
// @ts-ignore
|
||||
getSources({ query }) {
|
||||
return [
|
||||
{
|
||||
sourceId: 'pages',
|
||||
templates: {
|
||||
item({ item, components }) {
|
||||
return (
|
||||
<a
|
||||
href={item.url as string}
|
||||
className="aa-ItemLink truncate flex justify-between space-x-4"
|
||||
>
|
||||
<div className="aa-ItemContent w-full">
|
||||
<div className="aa-ItemTitle flex items-center space-x-1">
|
||||
{item.category && (
|
||||
<p
|
||||
className={`${
|
||||
['cli', 'api'].includes(item.category as string)
|
||||
? 'uppercase'
|
||||
: 'capitalize'
|
||||
}`}
|
||||
>
|
||||
<>
|
||||
{item.category}
|
||||
{item.version ? ` (${item.version})` : ''}:
|
||||
</>
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<components.Highlight hit={item} attribute="title" />
|
||||
</p>
|
||||
</div>
|
||||
<p className="aa-ItemContentSubtitle">{item.description as string}</p>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
},
|
||||
},
|
||||
getItemUrl({ item }) {
|
||||
return item.url
|
||||
},
|
||||
getItems() {
|
||||
// if (!query) return []
|
||||
return getAlgoliaResults({
|
||||
searchClient,
|
||||
queries: [
|
||||
{
|
||||
indexName: 'dev_docs',
|
||||
query,
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
return () => {
|
||||
search.destroy()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-[200px] relative">
|
||||
<div ref={searchRef} />
|
||||
<div className="flex items-center space-x-1 absolute top-[7px] right-2">
|
||||
<div className="text-scale-1200 flex items-center justify-center h-6 w-6 rounded bg-scale-500">
|
||||
<IconCommand size={12} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="text-xs text-scale-1200 flex items-center justify-center h-6 w-6 rounded bg-scale-500">
|
||||
K
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AlgoliaSearch
|
||||
@@ -1,216 +0,0 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState, useEffect, FC } from 'react'
|
||||
import { IconMenu, IconMoon, IconSearch, IconSun, IconCommand, Listbox } from 'ui'
|
||||
import { useTheme } from 'common/Providers'
|
||||
import { REFERENCES } from './Navigation.constants'
|
||||
|
||||
import { getPageType } from '~/lib/helpers'
|
||||
import SearchButton from '../Search/SearchButton'
|
||||
|
||||
const NavBar: FC = () => {
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
const { asPath, push } = useRouter()
|
||||
const pathSegments = asPath.split('/')
|
||||
|
||||
const library = pathSegments.length >= 3 ? pathSegments[2] : undefined
|
||||
const libraryMeta = REFERENCES?.[library] ?? undefined
|
||||
const versions = libraryMeta?.versions ?? []
|
||||
|
||||
const version = versions.includes(pathSegments[pathSegments.indexOf(library) + 1])
|
||||
? pathSegments[pathSegments.indexOf(library) + 1]
|
||||
: versions[0]
|
||||
|
||||
const pageType = getPageType(asPath)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [isDarkMode])
|
||||
|
||||
const pageLinks = [
|
||||
{ text: 'Guides', key: 'docs', link: '/' },
|
||||
{ text: 'Reference', key: 'reference', link: '/reference' },
|
||||
]
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
localStorage.setItem('supabaseDarkMode', (!isDarkMode).toString())
|
||||
toggleTheme()
|
||||
|
||||
const key = localStorage.getItem('supabaseDarkMode')
|
||||
document.documentElement.className = key === 'true' ? 'dark' : ''
|
||||
}
|
||||
|
||||
const onSelectVersion = (version: string) => {
|
||||
// [Joshen] Ideally we use <Link> but this works for now
|
||||
if (!library) return
|
||||
if (version === versions[0]) {
|
||||
push(`/reference/${library}`)
|
||||
} else {
|
||||
push(`/reference/${library}/${version}`)
|
||||
}
|
||||
}
|
||||
|
||||
// [Joshen] Kaizen: Use UI library's SidePanel for this
|
||||
const toggleMobileMenu = () => {
|
||||
setMobileMenuOpen(!mobileMenuOpen)
|
||||
const sidebar = document.querySelector('.sidebar-menu-container')
|
||||
const contentPane = document.querySelector('.main-content-pane')
|
||||
|
||||
sidebar.classList.toggle('hidden')
|
||||
contentPane.classList.toggle('hidden')
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={[
|
||||
'sticky top-0 z-10 flex items-center justify-between',
|
||||
'h-[60px] border-b bg-white px-4 backdrop-blur backdrop-filter',
|
||||
'dark:border-scale-400 dark:bg-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<button className="mr-4 block stroke-2 lg:hidden" onClick={toggleMobileMenu}>
|
||||
<IconMenu className="text-scale-1100" />
|
||||
</button>
|
||||
{mounted && (
|
||||
<Link href="/">
|
||||
<a className="flex items-center">
|
||||
<Image
|
||||
className="cursor-pointer"
|
||||
src={isDarkMode ? '/docs/supabase-dark.svg' : '/docs/supabase-light.svg'}
|
||||
width={124}
|
||||
height={24}
|
||||
alt="Supabase Logo"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<nav className="ml-8 hidden lg:flex">
|
||||
<ul className="hidden space-x-8 lg:flex">
|
||||
{pageLinks.map((p) => (
|
||||
<li key={`${p.text}-${p.link}`}>
|
||||
<Link href={p.link}>
|
||||
<a
|
||||
className={`text-sm ${
|
||||
pageType.includes(p.key) ? 'text-brand-900' : 'text-scale-1100'
|
||||
}`}
|
||||
>
|
||||
{p.text}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
{versions.length > 0 && (
|
||||
<div className="ml-8">
|
||||
<Listbox
|
||||
size="small"
|
||||
defaultValue={version}
|
||||
style={{ width: '70px' }}
|
||||
onChange={onSelectVersion}
|
||||
>
|
||||
{versions.map((version) => (
|
||||
<Listbox.Option key={version} label={version} value={version}>
|
||||
{version}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="hidden items-center md:flex">
|
||||
<ul className="flex items-center">
|
||||
<li className="px-4">
|
||||
<a
|
||||
href="https://app.supabase.com"
|
||||
className="text-scale-1100 text-sm"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li className="px-4">
|
||||
<a
|
||||
href="https://github.com/supabase/supabase"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<div
|
||||
className="h-4 w-4 !bg-no-repeat !bg-center"
|
||||
style={{
|
||||
background: isDarkMode
|
||||
? "url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E\") 50% no-repeat"
|
||||
: "url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E\")",
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li className="px-4">
|
||||
<a
|
||||
href="https://discord.supabase.com"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className="text-scale-1100"
|
||||
>
|
||||
<div
|
||||
className="h-4 w-4 !bg-no-repeat !bg-center"
|
||||
style={{
|
||||
background: isDarkMode
|
||||
? "url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 71 55' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23a)'%3E%3Cpath d='M60.105 4.898A58.55 58.55 0 0 0 45.653.415a.22.22 0 0 0-.233.11 40.784 40.784 0 0 0-1.8 3.697c-5.456-.817-10.886-.817-16.23 0-.485-1.164-1.201-2.587-1.828-3.697a.228.228 0 0 0-.233-.11 58.386 58.386 0 0 0-14.451 4.483.207.207 0 0 0-.095.082C1.578 18.73-.944 32.144.293 45.39a.244.244 0 0 0 .093.167c6.073 4.46 11.955 7.167 17.729 8.962a.23.23 0 0 0 .249-.082 42.08 42.08 0 0 0 3.627-5.9.225.225 0 0 0-.123-.312 38.772 38.772 0 0 1-5.539-2.64.228.228 0 0 1-.022-.378c.372-.279.744-.569 1.1-.862a.22.22 0 0 1 .23-.03c11.619 5.304 24.198 5.304 35.68 0a.219.219 0 0 1 .233.027c.356.293.728.586 1.103.865a.228.228 0 0 1-.02.378 36.384 36.384 0 0 1-5.54 2.637.227.227 0 0 0-.121.315 47.249 47.249 0 0 0 3.624 5.897.225.225 0 0 0 .249.084c5.801-1.794 11.684-4.502 17.757-8.961a.228.228 0 0 0 .092-.164c1.48-15.315-2.48-28.618-10.497-40.412a.18.18 0 0 0-.093-.084Zm-36.38 32.427c-3.497 0-6.38-3.211-6.38-7.156 0-3.944 2.827-7.156 6.38-7.156 3.583 0 6.438 3.24 6.382 7.156 0 3.945-2.827 7.156-6.381 7.156Zm23.593 0c-3.498 0-6.38-3.211-6.38-7.156 0-3.944 2.826-7.156 6.38-7.156 3.582 0 6.437 3.24 6.38 7.156 0 3.945-2.798 7.156-6.38 7.156Z' fill='%23fff'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='a'%3E%3Cpath fill='%23fff' d='M0 0h71v55H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E\") 50% no-repeat"
|
||||
: "url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 71 55' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23a)'%3E%3Cpath d='M60.105 4.898A58.55 58.55 0 0 0 45.653.415a.22.22 0 0 0-.233.11 40.784 40.784 0 0 0-1.8 3.697c-5.456-.817-10.886-.817-16.23 0-.485-1.164-1.201-2.587-1.828-3.697a.228.228 0 0 0-.233-.11 58.386 58.386 0 0 0-14.451 4.483.207.207 0 0 0-.095.082C1.578 18.73-.944 32.144.293 45.39a.244.244 0 0 0 .093.167c6.073 4.46 11.955 7.167 17.729 8.962a.23.23 0 0 0 .249-.082 42.08 42.08 0 0 0 3.627-5.9.225.225 0 0 0-.123-.312 38.772 38.772 0 0 1-5.539-2.64.228.228 0 0 1-.022-.378c.372-.279.744-.569 1.1-.862a.22.22 0 0 1 .23-.03c11.619 5.304 24.198 5.304 35.68 0a.219.219 0 0 1 .233.027c.356.293.728.586 1.103.865a.228.228 0 0 1-.02.378 36.384 36.384 0 0 1-5.54 2.637.227.227 0 0 0-.121.315 47.249 47.249 0 0 0 3.624 5.897.225.225 0 0 0 .249.084c5.801-1.794 11.684-4.502 17.757-8.961a.228.228 0 0 0 .092-.164c1.48-15.315-2.48-28.618-10.497-40.412a.18.18 0 0 0-.093-.084Zm-36.38 32.427c-3.497 0-6.38-3.211-6.38-7.156 0-3.944 2.827-7.156 6.38-7.156 3.583 0 6.438 3.24 6.382 7.156 0 3.945-2.827 7.156-6.381 7.156Zm23.593 0c-3.498 0-6.38-3.211-6.38-7.156 0-3.944 2.826-7.156 6.38-7.156 3.582 0 6.437 3.24 6.38 7.156 0 3.945-2.798 7.156-6.38 7.156Z' fill='%2323272A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='a'%3E%3Cpath fill='%23fff' d='M0 0h71v55H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E\")",
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li className="px-4">
|
||||
<a href="https://twitter.com/supabase" target="_blank" rel="noreferrer noopener">
|
||||
<div
|
||||
className="h-4 w-4 !bg-no-repeat !bg-center"
|
||||
style={{
|
||||
background: isDarkMode
|
||||
? "url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 335 276' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M302 70A195 195 0 0 1 3 245a142 142 0 0 0 97-30 70 70 0 0 1-58-47 70 70 0 0 0 31-2 70 70 0 0 1-57-66 70 70 0 0 0 28 5 70 70 0 0 1-18-90 195 195 0 0 0 141 72 67 67 0 0 1 116-62 117 117 0 0 0 43-17 65 65 0 0 1-31 38 117 117 0 0 0 39-11 65 65 0 0 1-32 35'/%3E%3C/svg%3E\") 50% no-repeat"
|
||||
: "url(\"data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 335 276' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M302 70A195 195 0 0 1 3 245a142 142 0 0 0 97-30 70 70 0 0 1-58-47 70 70 0 0 0 31-2 70 70 0 0 1-57-66 70 70 0 0 0 28 5 70 70 0 0 1-18-90 195 195 0 0 0 141 72 67 67 0 0 1 116-62 117 117 0 0 0 43-17 65 65 0 0 1-31 38 117 117 0 0 0 39-11 65 65 0 0 1-32 35'/%3E%3C/svg%3E\")",
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li className="px-4">
|
||||
<div className="cursor-pointer" onClick={toggleDarkMode}>
|
||||
{isDarkMode ? (
|
||||
<IconMoon size={18} strokeWidth={2} className="text-scale-1200" />
|
||||
) : (
|
||||
<IconSun size={18} strokeWidth={2} className="text-scale-1200" />
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<SearchButton>
|
||||
<div className="flex items-center justify-between space-x-6 bg-scale-300 border border-scale-700 pl-3 pr-1.5 py-1.5 rounded">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconSearch className="text-scale-1100" size={18} strokeWidth={2} />
|
||||
<p className="text-scale-800 text-sm">Search</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="hidden text-scale-1200 md:flex items-center justify-center h-6 w-6 rounded bg-scale-500">
|
||||
<IconCommand size={12} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="hidden text-xs text-scale-1200 md:flex items-center justify-center h-6 w-6 rounded bg-scale-500">
|
||||
K
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SearchButton>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
export default NavBar
|
||||
@@ -1,427 +0,0 @@
|
||||
// [Terry] Also need to look at getPageType() in ./lib/helpers
|
||||
// to set a menu to a page
|
||||
import SupabaseJsV1Nav from 'data/nav/supabase-js/v1'
|
||||
import SupabaseJsV2Nav from 'data/nav/supabase-js/v2'
|
||||
import SupabaseDartV0Nav from 'data/nav/supabase-dart/v0'
|
||||
import SupabaseDartV1Nav from 'data/nav/supabase-dart/v1'
|
||||
import SupabaseCLINav from 'data/nav/supabase-cli'
|
||||
import SupabaseAPINav from 'data/nav/supabase-api'
|
||||
import AuthServerNav from 'data/nav/auth-server'
|
||||
import RealtimeServerNav from 'data/nav/realtime-server'
|
||||
import StorageServerNav from 'data/nav/storage-server'
|
||||
|
||||
import { NavMenu, References } from './Navigation.types'
|
||||
|
||||
export const REFERENCES: References = {
|
||||
javascript: {
|
||||
name: 'supabase-js',
|
||||
library: 'supabase-js',
|
||||
versions: ['v2', 'v1'],
|
||||
icon: '/docs/img/icons/javascript-icon.svg',
|
||||
},
|
||||
dart: {
|
||||
name: 'Flutter',
|
||||
library: 'supabase-dart',
|
||||
versions: ['v1', 'v0'],
|
||||
icon: '/docs/img/icons/flutter-icon.svg',
|
||||
},
|
||||
cli: {
|
||||
name: 'CLI',
|
||||
library: undefined,
|
||||
versions: [],
|
||||
icon: '/docs/img/icons/cli-icon.svg',
|
||||
},
|
||||
api: {
|
||||
name: 'API',
|
||||
library: undefined,
|
||||
versions: [],
|
||||
icon: '/docs/img/icons/api-icon.svg',
|
||||
},
|
||||
}
|
||||
|
||||
export const menuItems: NavMenu = {
|
||||
docs: [
|
||||
{
|
||||
label: 'Overview',
|
||||
items: [
|
||||
{ name: 'Introduction', url: '/', items: [] },
|
||||
{ name: 'Features', url: '/features', items: [] },
|
||||
{ name: 'Architecture', url: '/architecture', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Quickstarts',
|
||||
items: [
|
||||
{ name: 'Angular', url: '/guides/with-angular', items: [] },
|
||||
{ name: 'Expo', url: '/guides/with-expo', items: [] },
|
||||
{ name: 'Flutter', url: '/guides/with-flutter', items: [] },
|
||||
{ name: 'Ionic Angular', url: '/guides/with-ionic-angular', items: [] },
|
||||
{ name: 'Ionic React', url: '/guides/with-ionic-react', items: [] },
|
||||
{ name: 'Ionic Vue', url: '/guides/with-ionic-vue', items: [] },
|
||||
{ name: 'Next.js', url: '/guides/with-nextjs', items: [] },
|
||||
{ name: 'Nuxt 3', url: '/guides/with-nuxt-3', items: [] },
|
||||
{ name: 'React', url: '/guides/with-react', items: [] },
|
||||
{ name: 'RedwoodJS', url: '/guides/with-redwoodjs', items: [] },
|
||||
{ name: 'SolidJS', url: '/guides/with-solidjs', items: [] },
|
||||
{ name: 'Svelte', url: '/guides/with-svelte', items: [] },
|
||||
{ name: 'SvelteKit', url: '/guides/with-sveltekit', items: [] },
|
||||
{ name: 'Vue 3', url: '/guides/with-vue-3', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'CLI',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/cli', items: [] },
|
||||
{ name: 'Local Development', url: '/guides/cli/local-development', items: [] },
|
||||
{ name: 'Managing Environments', url: '/guides/cli/managing-environments', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Auth',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/auth', items: [] },
|
||||
{
|
||||
name: 'Authentication',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Login with Email', url: '/guides/auth/auth-email', items: [] },
|
||||
{ name: 'Login with Magic Link', url: '/guides/auth/auth-magic-link', items: [] },
|
||||
{ name: 'Login with Apple', url: '/guides/auth/auth-apple', items: [] },
|
||||
{ name: 'Login with Azure', url: '/guides/auth/auth-azure', items: [] },
|
||||
{ name: 'Login with Bitbucket', url: '/guides/auth/auth-bitbucket', items: [] },
|
||||
{ name: 'Login with Discord', url: '/guides/auth/auth-discord', items: [] },
|
||||
{ name: 'Login with Facebook', url: '/guides/auth/auth-facebook', items: [] },
|
||||
{ name: 'Login with Github', url: '/guides/auth/auth-github', items: [] },
|
||||
{ name: 'Login with Gitlab', url: '/guides/auth/auth-gitlab', items: [] },
|
||||
{ name: 'Login with Google', url: '/guides/auth/auth-google', items: [] },
|
||||
{ name: 'Login with Keycloak', url: '/guides/auth/auth-keycloak', items: [] },
|
||||
{ name: 'Login with LinkedIn', url: '/guides/auth/auth-linkedin', items: [] },
|
||||
{ name: 'Login with Notion', url: '/guides/auth/auth-notion', items: [] },
|
||||
{ name: 'Login with Slack', url: '/guides/auth/auth-slack', items: [] },
|
||||
{ name: 'Login with Spotify', url: '/guides/auth/auth-spotify', items: [] },
|
||||
{ name: 'Login with Twitch', url: '/guides/auth/auth-twitch', items: [] },
|
||||
{ name: 'Login with Twitter', url: '/guides/auth/auth-twitter', items: [] },
|
||||
{ name: 'Login with WorkOS', url: '/guides/auth/auth-workos', items: [] },
|
||||
{ name: 'Login with Zoom', url: '/guides/auth/auth-zoom', items: [] },
|
||||
{ name: 'Phone Auth with Twilio', url: '/guides/auth/auth-twilio', items: [] },
|
||||
{ name: 'Phone Auth with Vonage', url: '/guides/auth/auth-vonage', items: [] },
|
||||
{
|
||||
name: 'Phone Auth with MessageBird',
|
||||
url: '/guides/auth/auth-messagebird',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Authorization',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Row Level Security', url: '/guides/auth/row-level-security', items: [] },
|
||||
{ name: 'Managing User Data', url: '/guides/auth/managing-user-data', items: [] },
|
||||
{ name: 'Enable Captcha Protection', url: '/guides/auth/auth-captcha', items: [] },
|
||||
{ name: 'Server-side Rendering', url: '/guides/auth/server-side-rendering', items: [] },
|
||||
{ name: 'Multi-Factor Authentication', url: '/guides/auth/auth-mfa', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Auth Helpers',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/auth/auth-helpers', items: [] },
|
||||
{ name: 'Auth UI', url: '/guides/auth/auth-helpers/auth-ui', items: [] },
|
||||
{ name: 'Next.js', url: '/guides/auth/auth-helpers/nextjs', items: [] },
|
||||
{
|
||||
name: 'Next.js Server Components',
|
||||
url: '/guides/auth/auth-helpers/nextjs-server-components',
|
||||
items: [],
|
||||
},
|
||||
{ name: 'SvelteKit', url: '/guides/auth/auth-helpers/sveltekit', items: [] },
|
||||
{ name: 'Remix', url: '/guides/auth/auth-helpers/remix', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Deep Dive',
|
||||
url: undefined,
|
||||
items: [
|
||||
{
|
||||
name: 'Part One: JWTs',
|
||||
url: '/learn/auth-deep-dive/auth-deep-dive-jwts',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Part Two: Row Level Security',
|
||||
url: '/learn/auth-deep-dive/auth-row-level-security',
|
||||
items: [],
|
||||
},
|
||||
{ name: 'Part Three: Policies', url: '/learn/auth-deep-dive/auth-policies', items: [] },
|
||||
{ name: 'Part Four: GoTrue', url: '/learn/auth-deep-dive/auth-gotrue', items: [] },
|
||||
{
|
||||
name: 'Part Five: Google OAuth',
|
||||
url: '/learn/auth-deep-dive/auth-google-oauth',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Database',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/database', items: [] },
|
||||
{ name: 'Database Connections', url: '/guides/database/connecting-to-postgres', items: [] },
|
||||
{ name: 'Tables and Data', url: '/guides/database/tables', items: [] },
|
||||
{ name: 'Database Functions', url: '/guides/database/functions', items: [] },
|
||||
{ name: 'Database Webhooks', url: '/guides/database/webhooks', items: [] },
|
||||
{ name: 'Full Text Search', url: '/guides/database/full-text-search', items: [] },
|
||||
{ name: 'Database Testing', url: '/guides/database/testing', items: [] },
|
||||
{
|
||||
name: 'Serverless APIs',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/database/api', items: [] },
|
||||
{ name: 'Generating Types', url: '/guides/database/api/generating-types', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Extensions',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/database/extensions', items: [] },
|
||||
{ name: 'http: RESTful Client', url: '/guides/database/extensions/http', items: [] },
|
||||
{
|
||||
name: 'pg_cron: Job Scheduling',
|
||||
url: '/guides/database/extensions/pgcron',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'pg_net: Async Networking',
|
||||
url: '/guides/database/extensions/pgnet',
|
||||
items: [],
|
||||
},
|
||||
{ name: 'pgTAP: Unit Testing', url: '/guides/database/extensions/pgtap', items: [] },
|
||||
{
|
||||
name: 'plv8: Javascript Language',
|
||||
url: '/guides/database/extensions/plv8',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'PostGIS: Geo queries',
|
||||
url: '/guides/database/extensions/postgis',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'uuid-ossp: Unique Identifiers',
|
||||
url: '/guides/database/extensions/uuid-ossp',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Configuration',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Timeouts', url: '/guides/database/timeouts', items: [] },
|
||||
{ name: 'Replication', url: '/guides/database/replication', items: [] },
|
||||
{ name: 'Passwords', url: '/guides/database/managing-passwords', items: [] },
|
||||
{ name: 'Timezones', url: '/guides/database/managing-timezones', items: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Edge Functions',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/functions', items: [] },
|
||||
{ name: 'Quickstart', url: '/guides/functions/quickstart', items: [] },
|
||||
{ name: 'Auth', url: '/guides/functions/auth', items: [] },
|
||||
{ name: 'Best Practices', url: '/guides/functions/best-practices', items: [] },
|
||||
{ name: 'CI/CD Workflow', url: '/guides/functions/cicd-workflow', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Realtime',
|
||||
items: [
|
||||
{
|
||||
name: 'Overview',
|
||||
url: '/guides/realtime',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Quickstart',
|
||||
url: '/guides/realtime/quickstart',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Features',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Channels', url: '/guides/realtime/channels', items: [] },
|
||||
{
|
||||
name: 'Extensions',
|
||||
url: '/guides/realtime/extensions',
|
||||
items: [
|
||||
{ name: 'Broadcast', url: '/guides/realtime/broadcast', items: [] },
|
||||
{ name: 'Presence', url: '/guides/realtime/presence', items: [] },
|
||||
{ name: 'Postgres Changes', url: '/guides/realtime/postgres-changes', items: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Guides',
|
||||
url: undefined,
|
||||
items: [
|
||||
{
|
||||
name: 'Subscribing to Database Changes',
|
||||
url: '/guides/realtime/subscribing-to-database-changes',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Using Realtime with Next.js',
|
||||
url: '/guides/realtime/realtime-with-nextjs',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Deep dive',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Rate Limits', url: '/guides/realtime/rate-limits', items: [] },
|
||||
{ name: 'Architecture', url: '/guides/realtime/architecture', items: [] },
|
||||
{ name: 'Protocol', url: '/guides/realtime/protocol', items: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Storage',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/storage', items: [] },
|
||||
{ name: 'Quickstart', url: '/guides/storage/quickstart', items: [] },
|
||||
{ name: 'Access Control', url: '/guides/storage/access-control', items: [] },
|
||||
{ name: 'CDN', url: '/guides/storage/cdn', items: [] },
|
||||
{ name: 'Image Transformations', url: '/guides/storage/image-transformations', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Platform',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/platform', items: [] },
|
||||
{ name: 'Compute Add-ons', url: '/guides/platform/compute-add-ons', items: [] },
|
||||
{ name: 'Custom Domains', url: '/guides/platform/custom-domains', items: [] },
|
||||
{ name: 'Database Usage', url: '/guides/platform/database-usage', items: [] },
|
||||
{ name: 'HTTP Status Codes', url: '/guides/platform/http-status-codes', items: [] },
|
||||
{ name: 'Logging', url: '/guides/platform/logs', items: [] },
|
||||
{ name: 'Metrics', url: '/guides/platform/metrics', items: [] },
|
||||
{
|
||||
name: 'Migrating and upgrading',
|
||||
url: '/guides/platform/migrating-and-upgrading-projects',
|
||||
items: [],
|
||||
},
|
||||
{ name: 'Network Restrictions', url: '/guides/platform/network-restrictions', items: [] },
|
||||
{ name: 'Performance Tuning', url: '/guides/platform/performance', items: [] },
|
||||
{ name: 'Permissions', url: '/guides/platform/permissions', items: [] },
|
||||
{ name: 'Production Readiness', url: '/guides/platform/going-into-prod', items: [] },
|
||||
{ name: 'SSL Enforcement', url: '/guides/platform/ssl-enforcement', items: [] },
|
||||
{ name: 'Troubleshooting', url: '/guides/platform/troubleshooting', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Self Hosting',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/hosting/overview', items: [] },
|
||||
{ name: 'Docker', url: '/guides/hosting/docker', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Migrate to Supabase',
|
||||
items: [
|
||||
{ name: 'Firebase Auth', url: '/guides/migrations/firebase-auth', items: [] },
|
||||
{ name: 'Firestore Data', url: '/guides/migrations/firestore-data', items: [] },
|
||||
{ name: 'Firebase Storage', url: '/guides/migrations/firebase-storage', items: [] },
|
||||
{ name: 'Heroku', url: '/guides/migrations/heroku', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Integrations',
|
||||
items: [
|
||||
{ name: 'Supabase Marketplace', url: '/guides/integrations/integrations', items: [] },
|
||||
{
|
||||
name: 'Auth',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Auth0', url: '/guides/integrations/auth0', items: [] },
|
||||
{ name: 'Authsignal', url: '/guides/integrations/authsignal', items: [] },
|
||||
{ name: 'Clerk', url: '/guides/integrations/clerk', items: [] },
|
||||
{ name: 'keyri', url: '/guides/integrations/keyri', items: [] },
|
||||
{ name: 'Picket', url: '/guides/integrations/picket', items: [] },
|
||||
{ name: 'Stytch', url: '/guides/integrations/stytch', items: [] },
|
||||
{ name: 'SuperTokens', url: '/guides/integrations/supertokens', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Caching / Offline-first',
|
||||
url: undefined,
|
||||
items: [{ name: 'Polyscale', url: '/guides/integrations/polyscale', items: [] }],
|
||||
},
|
||||
{
|
||||
name: 'Developer Tools',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Estuary', url: '/guides/integrations/estuary', items: [] },
|
||||
{ name: 'pgMustard', url: '/guides/integrations/pgmustard', items: [] },
|
||||
{ name: 'Prisma', url: '/guides/integrations/prisma', items: [] },
|
||||
{ name: 'Sequin', url: '/guides/integrations/sequin', items: [] },
|
||||
{ name: 'Snaplet', url: '/guides/integrations/snaplet', items: [] },
|
||||
{ name: 'Vercel', url: '/guides/integrations/vercel', items: [] },
|
||||
{ name: 'Zuplo', url: '/guides/integrations/zuplo', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Low-code',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Appsmith', url: '/guides/integrations/appsmith', items: [] },
|
||||
{ name: 'Dashibase', url: '/guides/integrations/dashibase', items: [] },
|
||||
{ name: 'DhiWise', url: '/guides/integrations/dhiwise', items: [] },
|
||||
{ name: 'Directus', url: '/guides/integrations/directus', items: [] },
|
||||
{ name: 'Draftbit', url: '/guides/integrations/draftbit', items: [] },
|
||||
{ name: 'Plasmic', url: '/guides/integrations/plasmic', items: [] },
|
||||
{ name: 'WeWeb', url: '/guides/integrations/weweb', items: [] },
|
||||
{ name: 'ILLA', url: '/guides/integrations/illa', items: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
reference: [
|
||||
{
|
||||
label: 'Official',
|
||||
items: [
|
||||
{ name: 'Reference Documentation', url: '/reference', items: [] },
|
||||
{ name: 'Supabase JavaScript Library', url: '/reference/javascript', items: [] },
|
||||
{ name: 'Supabase Flutter Library', url: '/reference/dart', items: [] },
|
||||
{ name: 'Supabase CLI', url: '/reference/cli', items: [] },
|
||||
{ name: 'Management API', url: '/reference/api', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Self-hosting',
|
||||
items: [
|
||||
{ name: 'Auth Server', url: '/reference/auth', items: [] },
|
||||
{ name: 'Realtime Server', url: '/reference/realtime', items: [] },
|
||||
{ name: 'Storage Server', url: '/reference/storage', items: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
'reference/javascript': SupabaseJsV2Nav,
|
||||
'reference/javascript/v1': SupabaseJsV1Nav,
|
||||
'reference/dart': SupabaseDartV1Nav,
|
||||
'reference/dart/v0': SupabaseDartV0Nav,
|
||||
'reference/cli': SupabaseCLINav,
|
||||
'reference/api': SupabaseAPINav,
|
||||
'reference/auth': AuthServerNav,
|
||||
'reference/realtime': RealtimeServerNav,
|
||||
'reference/storage': StorageServerNav,
|
||||
}
|
||||
@@ -27,10 +27,16 @@ const home = [
|
||||
href: '/guides/database',
|
||||
level: 'database',
|
||||
},
|
||||
{
|
||||
label: 'Serverless APIs',
|
||||
icon: '/img/icons/menu/reference',
|
||||
href: '/guides/api',
|
||||
level: 'api',
|
||||
},
|
||||
{
|
||||
label: 'Auth',
|
||||
icon: '/img/icons/menu/auth',
|
||||
href: '/guides/auth/overview',
|
||||
href: '/guides/auth',
|
||||
level: 'auth',
|
||||
},
|
||||
{
|
||||
@@ -109,6 +115,13 @@ const home = [
|
||||
level: 'reference_csharp',
|
||||
community: true,
|
||||
},
|
||||
{
|
||||
label: 'Swift',
|
||||
icon: '/img/icons/menu/reference-swift',
|
||||
href: '/reference/swift/introduction',
|
||||
level: 'reference_swift',
|
||||
community: true,
|
||||
},
|
||||
{
|
||||
label: 'Tools',
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -15,6 +15,8 @@ import spec_dart_v0 from '~/../../spec/supabase_dart_v0.yml' assert { type: 'yml
|
||||
import spec_csharp_v0 from '~/../../spec/supabase_csharp_v0.yml' assert { type: 'yml' }
|
||||
// @ts-expect-error
|
||||
import spec_python_v2 from '~/../../spec/supabase_py_v2.yml' assert { type: 'yml' }
|
||||
// @ts-expect-error
|
||||
import spec_swift_v1 from '~/../../spec/supabase_swift_v1.yml' assert { type: 'yml' }
|
||||
|
||||
// import { gen_v3 } from '~/lib/refGenerator/helpers'
|
||||
import apiCommonSections from '~/../../spec/common-api-sections.json'
|
||||
@@ -22,6 +24,7 @@ import cliCommonSections from '~/../../spec/common-cli-sections.json'
|
||||
import libCommonSections from '~/../../spec/common-client-libs-sections.json'
|
||||
import authServerCommonSections from '~/../../spec/common-self-hosting-auth-sections.json'
|
||||
import realtimeServerCommonSections from '~/../../spec/common-self-hosting-realtime-sections.json'
|
||||
import analyticsServerCommonSections from '~/../../spec/common-self-hosting-analytics-sections.json'
|
||||
import storageServerCommonSections from '~/../../spec/common-self-hosting-storage-sections.json'
|
||||
import { flattenSections } from '~/lib/helpers'
|
||||
import NavigationMenuHome from './HomeMenu'
|
||||
@@ -54,22 +57,26 @@ export type RefIdOptions =
|
||||
| 'reference_dart_v1'
|
||||
| 'reference_csharp_v0'
|
||||
| 'reference_python_v2'
|
||||
| 'reference_swift_v1'
|
||||
| 'reference_cli'
|
||||
| 'reference_api'
|
||||
| 'reference_self_hosting_auth'
|
||||
| 'reference_self_hosting_storage'
|
||||
| 'reference_self_hosting_realtime'
|
||||
| 'reference_self_hosting_analytics'
|
||||
|
||||
export type RefKeyOptions =
|
||||
| 'javascript'
|
||||
| 'dart'
|
||||
| 'csharp'
|
||||
| 'python'
|
||||
| 'swift'
|
||||
| 'cli'
|
||||
| 'api'
|
||||
| 'self-hosting-auth'
|
||||
| 'self-hosting-storage'
|
||||
| 'self-hosting-realtime'
|
||||
| 'self-hosting-analytics'
|
||||
|
||||
const NavigationMenu = () => {
|
||||
const router = useRouter()
|
||||
@@ -85,6 +92,9 @@ const NavigationMenu = () => {
|
||||
case url.includes(`/docs/guides/database`) && url:
|
||||
menuState.setMenuLevelId('database')
|
||||
break
|
||||
case url.includes(`/docs/guides/api`) && url:
|
||||
menuState.setMenuLevelId('api')
|
||||
break
|
||||
case url.includes(`/docs/guides/auth`) && url:
|
||||
menuState.setMenuLevelId('auth')
|
||||
break
|
||||
@@ -132,10 +142,14 @@ const NavigationMenu = () => {
|
||||
case url.includes(`/docs/reference/csharp`) && url:
|
||||
menuState.setMenuLevelId('reference_csharp_v0')
|
||||
break
|
||||
// puthon v2 (latest)
|
||||
// python v2 (latest)
|
||||
case url.includes(`/docs/reference/python`) && url:
|
||||
menuState.setMenuLevelId('reference_python_v2')
|
||||
break
|
||||
// swift v1 (latest)
|
||||
case url.includes(`/docs/reference/swift`) && url:
|
||||
menuState.setMenuLevelId('reference_swift_v1')
|
||||
break
|
||||
case url.includes(`/docs/reference/cli/config`) && url:
|
||||
menuState.setMenuLevelId('supabase_cli')
|
||||
break
|
||||
@@ -154,6 +168,9 @@ const NavigationMenu = () => {
|
||||
case url.includes(`/docs/reference/self-hosting-realtime`) && url:
|
||||
menuState.setMenuLevelId('reference_self_hosting_realtime')
|
||||
break
|
||||
case url.includes(`/docs/reference/self-hosting-analytics`) && url:
|
||||
menuState.setMenuLevelId('reference_self_hosting_analytics')
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
@@ -174,6 +191,7 @@ const NavigationMenu = () => {
|
||||
const isHomeActive = 'home' === level
|
||||
const isGettingStartedActive = 'gettingstarted' === level
|
||||
const isDatabaseActive = 'database' === level
|
||||
const isApiActive = 'api' === level
|
||||
const isAuthActive = 'auth' === level
|
||||
const isFunctionsActive = 'functions' === level
|
||||
const isRealtimeActive = 'realtime' === level
|
||||
@@ -191,11 +209,13 @@ const NavigationMenu = () => {
|
||||
const isReference_Dart_V1 = 'reference_dart_v1' === level
|
||||
const isReference_Csharp_V0 = 'reference_csharp_v0' === level
|
||||
const isReference_Python_V2 = 'reference_python_v2' === level
|
||||
const isReference_Swift_V1 = 'reference_swift_v1' === level
|
||||
const isReference_Cli = 'reference_cli' === level
|
||||
const isReference_Api = 'reference_api' === level
|
||||
const isReference_Self_Hosting_Auth = 'reference_self_hosting_auth' === level
|
||||
const isReference_Self_Hosting_Storage = 'reference_self_hosting_storage' === level
|
||||
const isReference_Self_Hosting_Realtime = 'reference_self_hosting_realtime' === level
|
||||
const isReference_Self_Hosting_Analytics = 'reference_self_hosting_analytics' === level
|
||||
|
||||
return (
|
||||
<div className={['flex relative', 'justify-center lg:justify-start'].join(' ')}>
|
||||
@@ -203,6 +223,7 @@ const NavigationMenu = () => {
|
||||
<NavigationMenuHome active={isHomeActive} />
|
||||
<NavigationMenuGuideList id={'gettingstarted'} active={isGettingStartedActive} />
|
||||
<NavigationMenuGuideList id={'database'} active={isDatabaseActive} />
|
||||
<NavigationMenuGuideList id={'api'} active={isApiActive} />
|
||||
<NavigationMenuGuideList id={'auth'} active={isAuthActive} />
|
||||
<NavigationMenuGuideList id={'functions'} active={isFunctionsActive} />
|
||||
<NavigationMenuGuideList
|
||||
@@ -258,7 +279,14 @@ const NavigationMenu = () => {
|
||||
lib="csharp"
|
||||
spec={spec_csharp_v0}
|
||||
/>
|
||||
|
||||
<NavigationMenuRefList
|
||||
key={'reference-swift-menu-v1'}
|
||||
id={'reference_swift_v1'}
|
||||
active={isReference_Swift_V1}
|
||||
commonSections={libCommonSections}
|
||||
lib="swift"
|
||||
spec={spec_swift_v1}
|
||||
/>
|
||||
<NavigationMenuRefList
|
||||
key={'reference-python-menu-v2'}
|
||||
id={'reference_python_v2'}
|
||||
@@ -304,6 +332,13 @@ const NavigationMenu = () => {
|
||||
commonSections={realtimeServerCommonSections}
|
||||
lib="self-hosting-realtime"
|
||||
/>
|
||||
<NavigationMenuRefList
|
||||
key={'reference-self-hosting-analytics-menu'}
|
||||
id={'reference_self_hosting_analytics'}
|
||||
active={isReference_Self_Hosting_Analytics}
|
||||
commonSections={analyticsServerCommonSections}
|
||||
lib="self-hosting-analytics"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'transition-all ml-8 duration-150 ease-out',
|
||||
'transition-all duration-150 ease-out',
|
||||
// enabled
|
||||
currentLevel === id && 'opacity-100 ml-0 delay-150 h-auto',
|
||||
// move menu back to margin-left
|
||||
|
||||
@@ -36,7 +36,7 @@ const NavigationMenuGuideList: React.FC<Props> = ({ id, active, value }) => {
|
||||
type={value ? 'multiple' : 'single'}
|
||||
value={value ?? firstLevelRoute}
|
||||
className={[
|
||||
'transition-all ml-8 duration-150 ease-out',
|
||||
'transition-all duration-150 ease-out',
|
||||
// enabled
|
||||
active && 'opacity-100 ml-0 delay-150',
|
||||
// level === 'home' && 'ml-12',
|
||||
|
||||
@@ -4,7 +4,6 @@ import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { IconChevronLeft } from '~/../../packages/ui'
|
||||
import * as NavItems from './NavigationMenu.constants'
|
||||
import * as Accordion from '@radix-ui/react-accordion'
|
||||
|
||||
const HeaderImage = React.memo(function HeaderImage(props: any) {
|
||||
@@ -21,7 +20,11 @@ const HeaderImage = React.memo(function HeaderImage(props: any) {
|
||||
)
|
||||
})
|
||||
|
||||
const HeaderLink = React.memo(function HeaderLink(props: any) {
|
||||
const HeaderLink = React.memo(function HeaderLink(props: {
|
||||
title: string
|
||||
id: string
|
||||
url: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,7 +32,7 @@ const NavigationMenuRefList: React.FC<INavigationMenuRefList> = ({
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'transition-all ml-8 duration-150 ease-out',
|
||||
'transition-all duration-150 ease-out',
|
||||
// enabled
|
||||
active && 'opacity-100 ml-0 delay-150 h-auto',
|
||||
// move menu back to margin-left
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { useTheme } from 'common/Providers'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { Button, IconCommand, IconMenu, IconMoon, IconSearch, IconSun, Input, Listbox } from 'ui'
|
||||
import { REFERENCES } from '~/components/Navigation/Navigation.constants'
|
||||
import { useTheme } from 'common/Providers'
|
||||
|
||||
import { getPageType } from '~/lib/helpers'
|
||||
import SearchButton from '~/components/Search/SearchButton'
|
||||
import {
|
||||
Button,
|
||||
IconCommand,
|
||||
IconMenu,
|
||||
IconMoon,
|
||||
IconSearch,
|
||||
IconSun,
|
||||
Listbox,
|
||||
SearchButton,
|
||||
} from 'ui'
|
||||
import { REFERENCES } from './NavigationMenu.constants'
|
||||
|
||||
const TopNavBar: FC = () => {
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
@@ -25,25 +31,10 @@ const TopNavBar: FC = () => {
|
||||
? pathSegments[pathSegments.indexOf(library) + 1]
|
||||
: versions[0]
|
||||
|
||||
const pageType = getPageType(asPath)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [isDarkMode])
|
||||
|
||||
const pageLinks = [
|
||||
{ text: 'Guides', key: 'docs', link: '/' },
|
||||
{ text: 'Reference', key: 'reference', link: '/reference' },
|
||||
]
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
localStorage.setItem('supabaseDarkMode', (!isDarkMode).toString())
|
||||
toggleTheme()
|
||||
|
||||
const key = localStorage.getItem('supabaseDarkMode')
|
||||
document.documentElement.className = key === 'true' ? 'dark' : ''
|
||||
}
|
||||
|
||||
const onSelectVersion = (version: string) => {
|
||||
// [Joshen] Ideally we use <Link> but this works for now
|
||||
if (!library) return
|
||||
@@ -147,7 +138,7 @@ const TopNavBar: FC = () => {
|
||||
</Button>
|
||||
<ul className="flex items-center">
|
||||
<li className="px-4">
|
||||
<div className="cursor-pointer" onClick={toggleDarkMode}>
|
||||
<div className="cursor-pointer" onClick={() => toggleTheme()}>
|
||||
{isDarkMode ? (
|
||||
<IconMoon
|
||||
size={16}
|
||||
|
||||
@@ -3,11 +3,10 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { Button, IconCommand, IconGitHub, IconMoon, IconSearch, IconSun } from 'ui'
|
||||
import { REFERENCES } from '~/components/Navigation/Navigation.constants'
|
||||
import { Button, IconCommand, IconGitHub, IconMoon, IconSearch, IconSun, SearchButton } from 'ui'
|
||||
|
||||
import SearchButton from '~/components/Search/SearchButton'
|
||||
import { getPageType } from '~/lib/helpers'
|
||||
import { REFERENCES } from './NavigationMenu.constants'
|
||||
|
||||
const TopNavBarRef: FC = () => {
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
@@ -36,15 +35,6 @@ const TopNavBarRef: FC = () => {
|
||||
{ text: 'Reference', key: 'reference', link: '/reference' },
|
||||
]
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
localStorage.setItem('supabaseDarkMode', (!isDarkMode).toString())
|
||||
toggleTheme()
|
||||
|
||||
const key = localStorage.getItem('supabaseDarkMode')
|
||||
document.documentElement.className = key === 'true' ? 'dark' : ''
|
||||
document.documentElement.style.colorScheme = key === 'true' ? 'dark' : ''
|
||||
}
|
||||
|
||||
const onSelectVersion = (version: string) => {
|
||||
// [Joshen] Ideally we use <Link> but this works for now
|
||||
if (!library) return
|
||||
@@ -136,20 +126,19 @@ const TopNavBarRef: FC = () => {
|
||||
Dashboard
|
||||
</Button>
|
||||
<ul className="flex items-center">
|
||||
<Button
|
||||
type="text"
|
||||
as="a"
|
||||
// @ts-ignore
|
||||
<Link
|
||||
href="https://github.com/supabase/supabase"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<IconGitHub size={16} />
|
||||
</Button>
|
||||
<a className="px-2.5 py-1" target="_blank">
|
||||
<IconGitHub size={16} />
|
||||
</a>
|
||||
</Link>
|
||||
</ul>
|
||||
<ul className="flex items-center">
|
||||
<li className="px-4">
|
||||
<div className="cursor-pointer" onClick={toggleDarkMode}>
|
||||
<div className="cursor-pointer" onClick={() => toggleTheme()}>
|
||||
{isDarkMode ? (
|
||||
<IconMoon
|
||||
size={16}
|
||||
|
||||
@@ -2,7 +2,8 @@ import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/router'
|
||||
import { IconChevronRight, IconArrowLeft } from '~/../../packages/ui'
|
||||
import { REFERENCES } from './Navigation.constants'
|
||||
import { REFERENCES } from './NavigationMenu/NavigationMenu.constants'
|
||||
|
||||
import { NavMenuGroup, NavMenuSection } from './Navigation.types'
|
||||
import * as Accordion from '@radix-ui/react-accordion'
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import {
|
||||
ButtonHTMLAttributes,
|
||||
DetailedHTMLProps,
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useActionKey } from '~/hooks/useActionKey'
|
||||
import { useSearch } from './SearchProvider'
|
||||
|
||||
const SearchButton: FC<
|
||||
PropsWithChildren<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>>
|
||||
> = ({ children, ...props }) => {
|
||||
const searchButtonRef = useRef<HTMLButtonElement>()
|
||||
const { open, setQuery } = useSearch()
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (searchButtonRef.current === document.activeElement) {
|
||||
if (/[a-zA-Z0-9]/.test(String.fromCharCode(event.keyCode))) {
|
||||
open()
|
||||
setQuery?.(event.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}, [open, setQuery])
|
||||
|
||||
return (
|
||||
<button type="button" ref={searchButtonRef} onClick={open} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchButton
|
||||
@@ -1,423 +0,0 @@
|
||||
import type { CreateCompletionResponse } from 'openai'
|
||||
import { FC, useCallback, useRef, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { SSE } from 'sse.js'
|
||||
import clippyImageDark from '../../public/img/clippy-dark.png'
|
||||
import clippyImage from '../../public/img/clippy.png'
|
||||
|
||||
import { useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import { useTheme } from 'common/Providers'
|
||||
import Image from 'next/image'
|
||||
import {
|
||||
Button,
|
||||
IconAlertCircle,
|
||||
IconAlertTriangle,
|
||||
IconLoader,
|
||||
IconSearch,
|
||||
Input,
|
||||
Loading,
|
||||
Modal,
|
||||
Tabs,
|
||||
} from 'ui'
|
||||
import components from '~/components'
|
||||
import { IS_PLATFORM } from '~/lib/constants'
|
||||
import { useSearch } from './SearchProvider'
|
||||
import SearchResult, { SearchResultType } from './SearchResult'
|
||||
|
||||
const questions = [
|
||||
'How do I get started with Supabase?',
|
||||
'How do I run Supabase locally?',
|
||||
'How do I connect to my database?',
|
||||
'How do I run migrations? ',
|
||||
'How do I listen to changes in a table?',
|
||||
'How do I set up authentication?',
|
||||
]
|
||||
|
||||
function getEdgeFunctionUrl() {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '')
|
||||
|
||||
if (IS_PLATFORM) {
|
||||
const [schemeAndProjectId, domain, tld] = supabaseUrl.split('.')
|
||||
return `${schemeAndProjectId}.functions.${domain}.${tld}`
|
||||
} else {
|
||||
return `${supabaseUrl}/functions/v1`
|
||||
}
|
||||
}
|
||||
|
||||
const edgeFunctionUrl = getEdgeFunctionUrl()
|
||||
|
||||
const SearchModal: FC = () => {
|
||||
const { isDarkMode } = useTheme()
|
||||
const { close, query, setQuery } = useSearch()
|
||||
const [answer, setAnswer] = useState('')
|
||||
const [results, setResults] = useState<any[]>()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isResponding, setIsResponding] = useState(false)
|
||||
const [hasClippyError, setHasClippyError] = useState(false)
|
||||
const [hasSearchError, setHasSearchError] = useState(false)
|
||||
const [selectedTab, setSelectedTab] = useState('search-panel')
|
||||
const eventSourceRef = useRef<SSE>()
|
||||
const supabaseClient = useSupabaseClient()
|
||||
|
||||
const cantHelp = answer?.trim() === "Sorry, I don't know how to help with that."
|
||||
const status = isLoading
|
||||
? 'Clippy is searching...'
|
||||
: isResponding
|
||||
? 'Clippy is responding...'
|
||||
: cantHelp || hasClippyError
|
||||
? 'Clippy has failed you'
|
||||
: undefined
|
||||
|
||||
const handleSearchConfirm = useCallback(
|
||||
async (query: string) => {
|
||||
setResults(undefined)
|
||||
setAnswer(undefined)
|
||||
setIsResponding(false)
|
||||
setHasClippyError(false)
|
||||
setHasSearchError(false)
|
||||
setIsLoading(true)
|
||||
|
||||
const { error, data: pageSections } = await supabaseClient.functions.invoke('search', {
|
||||
body: { query },
|
||||
})
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
if (error) {
|
||||
setIsLoading(false)
|
||||
setIsResponding(false)
|
||||
setHasSearchError(true)
|
||||
console.error(error)
|
||||
return
|
||||
}
|
||||
|
||||
if (!Array.isArray(pageSections)) {
|
||||
setIsLoading(false)
|
||||
setIsResponding(false)
|
||||
setHasSearchError(true)
|
||||
console.error('Malformed response')
|
||||
return
|
||||
}
|
||||
|
||||
setResults(pageSections)
|
||||
},
|
||||
[supabaseClient]
|
||||
)
|
||||
|
||||
const handleClippyConfirm = useCallback(async (query: string) => {
|
||||
setResults(undefined)
|
||||
setAnswer(undefined)
|
||||
setIsResponding(false)
|
||||
setHasClippyError(false)
|
||||
setHasSearchError(false)
|
||||
setIsLoading(true)
|
||||
|
||||
const eventSource = new SSE(`${edgeFunctionUrl}/clippy-search`, {
|
||||
headers: {
|
||||
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||
Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
payload: JSON.stringify({ query }),
|
||||
})
|
||||
|
||||
function handleError<T>(err: T) {
|
||||
setIsLoading(false)
|
||||
setIsResponding(false)
|
||||
setHasClippyError(true)
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
eventSource.addEventListener('error', handleError)
|
||||
eventSource.addEventListener('message', (e) => {
|
||||
try {
|
||||
setIsLoading(false)
|
||||
|
||||
if (e.data === '[DONE]') {
|
||||
setIsResponding(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsResponding(true)
|
||||
|
||||
const completionResponse: CreateCompletionResponse = JSON.parse(e.data)
|
||||
const [{ text }] = completionResponse.choices
|
||||
|
||||
setAnswer((answer) => {
|
||||
return (answer ?? '') + text
|
||||
})
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
})
|
||||
|
||||
eventSource.stream()
|
||||
|
||||
eventSourceRef.current = eventSource
|
||||
|
||||
setIsLoading(true)
|
||||
}, [])
|
||||
|
||||
const handleConfirm = useCallback(
|
||||
(selectedTab: string, query: string) => {
|
||||
switch (selectedTab) {
|
||||
case 'search-panel':
|
||||
return handleSearchConfirm(query)
|
||||
case 'clippy-panel':
|
||||
return handleClippyConfirm(query)
|
||||
}
|
||||
},
|
||||
[handleSearchConfirm, handleClippyConfirm]
|
||||
)
|
||||
|
||||
function handleResetPrompt() {
|
||||
eventSourceRef.current?.close()
|
||||
eventSourceRef.current = undefined
|
||||
setQuery('')
|
||||
setResults(undefined)
|
||||
setAnswer(undefined)
|
||||
setIsResponding(false)
|
||||
setHasClippyError(false)
|
||||
setHasSearchError(false)
|
||||
}
|
||||
return (
|
||||
<Modal size="xlarge" visible={true} onCancel={close} closable={false} hideFooter>
|
||||
<div
|
||||
className={`mx-auto max-h-[90vh] lg:max-h-[75vh] flex flex-col gap-4 rounded-lg p-4 md:pt-6 md:px-6 pb-2 w-full shadow-2xl overflow-hidden border text-left border-scale-500 bg-scale-100 dark:bg-scale-300 cursor-auto relative min-w-[340px]`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="w-full"
|
||||
size="xlarge"
|
||||
autoFocus
|
||||
placeholder={selectedTab === 'search-panel' ? 'Search documentation' : 'Ask a question'}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
icon={<IconSearch size="small" />}
|
||||
onKeyDown={(e) => {
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
if (!query) {
|
||||
return
|
||||
}
|
||||
handleConfirm(selectedTab, query)
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="absolute right-0 top-0 mt-3 mr-4 hidden md:block">
|
||||
<Button type="default" size="tiny" onClick={close}>
|
||||
esc
|
||||
</Button>
|
||||
</div>
|
||||
{!isLoading && answer && (
|
||||
<div className="absolute right-0 top-0 mt-3 mr-16 hidden md:block">
|
||||
<Button type="text" size="tiny" onClick={handleResetPrompt}>
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tabs
|
||||
activeId={selectedTab}
|
||||
onChange={(tabId) => {
|
||||
setSelectedTab(tabId)
|
||||
if (!query) {
|
||||
handleResetPrompt()
|
||||
return
|
||||
}
|
||||
handleConfirm(tabId, query)
|
||||
}}
|
||||
>
|
||||
<Tabs.Panel id="search-panel" label="Guides & Reference">
|
||||
<div className="mb-6">
|
||||
{!isLoading && !hasSearchError && !results && (
|
||||
<div className="p-10 grid">
|
||||
<h2 className="text-lg text-center text-scale-1100">
|
||||
Search Supabase guides & reference docs
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
{results && results.length > 0 && (
|
||||
<div className="flex flex-col gap-3 max-h-[70vh] lg:max-h-[50vh] overflow-y-auto px-4 py-4 rounded-lg bg-scale-200">
|
||||
{results.map((page) => {
|
||||
const pageSections = page.sections.filter((section) => !!section.heading)
|
||||
return (
|
||||
<div key={page.id} className="flex flex-col gap-3">
|
||||
<SearchResult
|
||||
href={page.path}
|
||||
type={SearchResultType.Document}
|
||||
title={page.meta.title}
|
||||
/>
|
||||
{pageSections.length > 0 && (
|
||||
<div className="flex flex-row">
|
||||
<div className="border bg-scale-300 rounded-xl self-stretch p-[1px] ml-4 mr-4"></div>
|
||||
<div className="flex flex-col gap-3 items-stretch grow">
|
||||
{pageSections.map((section) => (
|
||||
<SearchResult
|
||||
key={section.id}
|
||||
href={`${page.path}${page.type === 'reference' ? '/' : '#'}${
|
||||
section.slug
|
||||
}`}
|
||||
type={SearchResultType.Section}
|
||||
title={section.heading}
|
||||
chip={page.meta.title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="p-6 grid gap-6 mt-4">
|
||||
<Loading active>{}</Loading>
|
||||
<p className="text-lg text-center">Searching for results</p>
|
||||
</div>
|
||||
)}
|
||||
{results && results.length === 0 && (
|
||||
<div className="p-6 flex flex-col items-center gap-6 mt-4">
|
||||
<IconAlertTriangle strokeWidth={1.5} size={40} />
|
||||
<p className="text-lg text-center">No results found.</p>
|
||||
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
|
||||
Try again?
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{hasSearchError && (
|
||||
<div className="p-6 flex flex-col items-center gap-6 mt-4">
|
||||
<IconAlertTriangle strokeWidth={1.5} size={40} />
|
||||
<p className="text-lg text-center">
|
||||
Sorry, looks like we're having some issues with search!
|
||||
</p>
|
||||
<p className="text-sm text-center">Please try again in a bit.</p>
|
||||
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
|
||||
Try again?
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="clippy-panel" label="Ask Clippy">
|
||||
{!isLoading && !answer && !hasClippyError && (
|
||||
<div className="">
|
||||
<div className="mt-2">
|
||||
<h2 className="text-sm text-scale-1100">Not sure where to start?</h2>
|
||||
|
||||
<ul className="text-sm mt-4 text-scale-1100 grid md:flex gap-4 flex-wrap max-w-3xl">
|
||||
{questions.map((question) => {
|
||||
const key = question.replace(/\s+/g, '_')
|
||||
return (
|
||||
<li key={key}>
|
||||
<button
|
||||
className="hover:bg-slate-400 hover:dark:bg-slate-400 px-4 py-2 bg-slate-300 dark:bg-slate-200 rounded-lg transition-colors"
|
||||
onClick={() => {
|
||||
setQuery(question)
|
||||
handleClippyConfirm(question)
|
||||
}}
|
||||
>
|
||||
{question}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{answer && (
|
||||
<div className="px-4 py-4 rounded-lg overflow-y-auto bg-scale-200 max-h-[70vh] lg:max-h-[50vh]">
|
||||
{cantHelp ? (
|
||||
<p className="flex flex-col gap-4 items-center p-4">
|
||||
<div className="grid md:flex items-center gap-2 mt-4 text-center justify-items-center">
|
||||
<IconAlertCircle />
|
||||
<p>Sorry, I don't know how to help with that.</p>
|
||||
</div>
|
||||
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
|
||||
Try again?
|
||||
</Button>
|
||||
</p>
|
||||
) : (
|
||||
<div className="prose dark:prose-dark">
|
||||
<ReactMarkdown
|
||||
linkTarget="_blank"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
transformLinkUri={(href) => {
|
||||
const supabaseUrl = new URL('https://supabase.com')
|
||||
const linkUrl = new URL(href, 'https://supabase.com')
|
||||
|
||||
if (linkUrl.origin === supabaseUrl.origin) {
|
||||
return linkUrl.toString()
|
||||
}
|
||||
|
||||
return href
|
||||
}}
|
||||
components={components}
|
||||
>
|
||||
{answer}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="p-6 grid gap-6 mt-4">
|
||||
<Loading active>{}</Loading>
|
||||
<p className="text-lg text-center">Searching for results</p>
|
||||
</div>
|
||||
)}
|
||||
{hasClippyError && (
|
||||
<div className="p-6 flex flex-col items-center gap-6 mt-4">
|
||||
<IconAlertTriangle strokeWidth={1.5} size={40} />
|
||||
<p className="text-lg text-center">
|
||||
Sorry, looks like Clippy is having a hard time!
|
||||
</p>
|
||||
<p className="text-sm text-center">Please try again in a bit.</p>
|
||||
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
|
||||
Try again?
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-scale-600 mt-4 text-scale-1100">
|
||||
<div className="flex justify-between items-center py-2 text-xs">
|
||||
<div className="flex items-centerp gap-1 pt-3 pb-1">
|
||||
<span>Powered by OpenAI.</span>
|
||||
<a href="/blog/chatgpt-supabase-docs" className="underline">
|
||||
Read the blog post
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 py-1">
|
||||
{status ? (
|
||||
<span className="bg-scale-400 rounded-lg py-1 px-2 items-center gap-2 hidden md:flex">
|
||||
{(isLoading || isResponding) && (
|
||||
<IconLoader size={14} className="animate-spin" />
|
||||
)}
|
||||
{status}
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Image
|
||||
width={30}
|
||||
height={34}
|
||||
src={isDarkMode ? clippyImageDark : clippyImage}
|
||||
alt="Clippy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchModal
|
||||
@@ -1,80 +0,0 @@
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import SearchModal from './SearchModal'
|
||||
|
||||
export type SearchContextValue = {
|
||||
isOpen: boolean
|
||||
open: () => void
|
||||
close: () => void
|
||||
query: string
|
||||
setQuery: (query: string) => void
|
||||
}
|
||||
|
||||
export const SearchContext = createContext<SearchContextValue>(null)
|
||||
|
||||
export const useSearch = () => {
|
||||
const { isOpen, open, close, query, setQuery } = useContext(SearchContext)
|
||||
|
||||
return { isOpen, open, close, query, setQuery }
|
||||
}
|
||||
|
||||
const SearchProvider: FC<PropsWithChildren<{}>> = ({ children }) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const open = useCallback(() => {
|
||||
setIsOpen(true)
|
||||
document.body.classList.add('DocSearch--active')
|
||||
}, [])
|
||||
|
||||
const close = useCallback(() => {
|
||||
setIsOpen(false)
|
||||
document.body.classList.remove('DocSearch--active')
|
||||
}, [])
|
||||
|
||||
useSearchKeyboardEvents({
|
||||
open,
|
||||
close,
|
||||
})
|
||||
|
||||
return (
|
||||
<SearchContext.Provider value={{ isOpen, open, close, query, setQuery }}>
|
||||
{children}
|
||||
{isOpen && createPortal(<SearchModal />, document.body)}
|
||||
</SearchContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function useSearchKeyboardEvents({ open, close }) {
|
||||
useEffect(() => {
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
close()
|
||||
return
|
||||
case 'k':
|
||||
case '/':
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
open()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}, [open, close])
|
||||
}
|
||||
|
||||
export default SearchProvider
|
||||
@@ -1,54 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import { FC } from 'react'
|
||||
import { IconBookOpen, IconHash } from '~/../../packages/ui'
|
||||
import { useSearch } from './SearchProvider'
|
||||
|
||||
export enum SearchResultType {
|
||||
Document = 'document',
|
||||
Section = 'section',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
href: string
|
||||
type: SearchResultType
|
||||
title: string
|
||||
chip?: string
|
||||
}
|
||||
|
||||
const SearchResult: FC<Props> = ({ href, type, title, chip }) => {
|
||||
const { close } = useSearch()
|
||||
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a
|
||||
className="flex flex-row items-center bg-scale-400 hover:bg-scale-600 transition p-4 rounded-md border border-scale-600 text-sm cursor-pointer"
|
||||
onClick={close}
|
||||
>
|
||||
<div className="w-6 h-6 p-1 flex items-center justify-center mr-4 text-brand-1100 rounded-md bg-scale-700">
|
||||
{getIconByType(type)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-start">
|
||||
{chip && (
|
||||
<div className="rounded-xl bg-scale-700 pl-3 pr-3 pt-0.5 pb-0.5 text-xs text-scale-1100">
|
||||
{chip}
|
||||
</div>
|
||||
)}
|
||||
<div>{title}</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function getIconByType(type: SearchResultType) {
|
||||
switch (type) {
|
||||
case SearchResultType.Document:
|
||||
return <IconBookOpen />
|
||||
case SearchResultType.Section:
|
||||
return <IconHash />
|
||||
default:
|
||||
throw new Error(`Unknown search result type '${type}'`)
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchResult
|
||||
@@ -1,31 +1,27 @@
|
||||
import { Button, Tabs, Alert, GlassPanel } from 'ui'
|
||||
import Link from 'next/link'
|
||||
import { Alert, Button, CodeBlock, GlassPanel, markdownComponents, Tabs } from 'ui'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
// Common components
|
||||
import Admonition from './Admonition'
|
||||
import ButtonCard from './ButtonCard'
|
||||
import CodeBlock from './CodeBlock/CodeBlock'
|
||||
import { parseNumericRange } from './CodeBlock/CodeBlock.utils'
|
||||
import JwtGenerator from './JwtGenerator'
|
||||
|
||||
// Page specific components
|
||||
import Frameworks from './Frameworks'
|
||||
import AuthProviders from './AuthProviders'
|
||||
import FunctionsExamples from './FunctionsExamples'
|
||||
import Extensions from './Extensions'
|
||||
import Frameworks from './Frameworks'
|
||||
import FunctionsExamples from './FunctionsExamples'
|
||||
|
||||
// Other components
|
||||
import { Heading } from './CustomHTMLElements'
|
||||
import QuickstartIntro from './MDX/quickstart_intro.mdx'
|
||||
import ProjectSetup from './MDX/project_setup.mdx'
|
||||
import SocialProviderSetup from './MDX/social_provider_setup.mdx'
|
||||
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
|
||||
import StorageManagement from './MDX/storage_management.mdx'
|
||||
import { Mermaid } from 'mdx-mermaid/lib/Mermaid'
|
||||
import InlineCodeTag from './CustomHTMLElements/InlineCode'
|
||||
import React from 'react'
|
||||
import RefSubLayout from '~/layouts/ref/RefSubLayout'
|
||||
import { Heading } from './CustomHTMLElements'
|
||||
import ProjectSetup from './MDX/project_setup.mdx'
|
||||
import QuickstartIntro from './MDX/quickstart_intro.mdx'
|
||||
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
|
||||
import SocialProviderSetup from './MDX/social_provider_setup.mdx'
|
||||
import StorageManagement from './MDX/storage_management.mdx'
|
||||
// import { CH } from '@code-hike/mdx/components'
|
||||
import RefHeaderSection from './reference/RefHeaderSection'
|
||||
|
||||
@@ -34,9 +30,9 @@ import CliGlobalFlagsHandler from '~/components/reference/enrichments/cli/CliGlo
|
||||
|
||||
import Options from '~/components/Options'
|
||||
import Param from '~/components/Params'
|
||||
import Image from 'next/image'
|
||||
|
||||
const components = {
|
||||
...markdownComponents,
|
||||
Admonition,
|
||||
Button,
|
||||
ButtonCard,
|
||||
@@ -72,26 +68,11 @@ const components = {
|
||||
{props.children}
|
||||
</Heading>
|
||||
),
|
||||
// pre: (props: any) => {
|
||||
// const linesToHighlight = parseNumericRange(props.lines ?? '')
|
||||
// return <CodeBlock {...props} linesToHighlight={linesToHighlight} />
|
||||
// },
|
||||
mono: (props: any) => <code className="text-sm">{props.children}</code>,
|
||||
// inlineCode: (props: { children: string }) => <InlineCodeTag {...props} />,
|
||||
RefSubLayout,
|
||||
// CH,
|
||||
code: (props: any) => <CodeBlock {...props} />,
|
||||
RefHeaderSection: (props: any) => <RefHeaderSection {...props} />,
|
||||
CliGlobalFlagsHandler: () => <CliGlobalFlagsHandler />,
|
||||
Options,
|
||||
Param,
|
||||
img: (props: any) => {
|
||||
return (
|
||||
<span className={['next-image--dynamic-fill'].join(' ')}>
|
||||
<Image {...props} className={['rounded-md border'].join(' ')} layout="fill" />
|
||||
</span>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default components
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Tabs } from '~/../../packages/ui'
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
import { CodeBlock, Tabs } from 'ui'
|
||||
import Param from '~/components/Params'
|
||||
import RefSubLayout from '~/layouts/ref/RefSubLayout'
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { IconChevronRight, Tabs } from '~/../../packages/ui'
|
||||
import { CodeBlock, IconChevronRight, Tabs } from 'ui'
|
||||
// @ts-expect-error
|
||||
import spec from '~/../../spec/cli_v1_commands.yaml' assert { type: 'yml' }
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
import Options from '~/components/Options'
|
||||
import Param from '~/components/Params'
|
||||
import RefSubLayout from '~/layouts/ref/RefSubLayout'
|
||||
@@ -15,6 +14,8 @@ export type Flag = {
|
||||
default_value: string
|
||||
accepted_values: AcceptedValue[]
|
||||
required?: boolean
|
||||
/** Whether subcommands inherit this flag. */
|
||||
inherit?: boolean
|
||||
}
|
||||
|
||||
export type AcceptedValue = {
|
||||
@@ -47,6 +48,14 @@ export type Command = {
|
||||
|
||||
const CliCommandSection = (props) => {
|
||||
const command = spec.commands.find((x: any) => x.id === props.funcData.id)
|
||||
const parentCommand = spec.commands.find(
|
||||
(x: any) => x.subcommands && x.subcommands.find((y: any) => y === props.funcData.id)
|
||||
)
|
||||
|
||||
const commandFlags = [
|
||||
...(parentCommand?.flags?.filter((x: any) => x.inherit) || []),
|
||||
...command.flags,
|
||||
]
|
||||
|
||||
return (
|
||||
<RefSubLayout.Section
|
||||
@@ -77,7 +86,7 @@ const CliCommandSection = (props) => {
|
||||
)}
|
||||
</header>
|
||||
|
||||
{command.subcommands.length > 0 && (
|
||||
{command.subcommands?.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<h3 className="text-lg text-scale-1200 mb-3">Available Commands</h3>
|
||||
<ul>
|
||||
@@ -97,11 +106,11 @@ const CliCommandSection = (props) => {
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{command.flags.length > 0 && (
|
||||
{commandFlags.length > 0 && (
|
||||
<>
|
||||
<h3 className="text-lg text-scale-1200 mb-3">Flags</h3>
|
||||
<ul className="">
|
||||
{command.flags.map((flag: Flag) => (
|
||||
{commandFlags.map((flag: Flag) => (
|
||||
<>
|
||||
<li className="mt-0">
|
||||
<Param {...flag} isOptional={!flag.required}>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
import { IconDatabase, Tabs } from 'ui'
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
import { CodeBlock, IconDatabase, Tabs } from 'ui'
|
||||
|
||||
import Options from '~/components/Options'
|
||||
import Param from '~/components/Params'
|
||||
|
||||
@@ -160,6 +160,11 @@ const Nav = [
|
||||
url: '/reference/javascript/storage-from-createsignedurls',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'from.createSignedUploadUrl()',
|
||||
url: '/reference/javascript/storage-from-createsigneduploadurl',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'from.getPublicUrl()',
|
||||
url: '/reference/javascript/storage-from-getpublicurl',
|
||||
|
||||
@@ -19,3 +19,14 @@ hideTitle: true
|
||||
changes, invoke Deno Edge Functions, build login and user management functionality, and manage
|
||||
large files.
|
||||
</div>
|
||||
|
||||
<div className="max-w-xl bg-slate-300 px-4 py-2 rounded-md">
|
||||
<p>The Python client library is created and maintained by the Supabase community, and is not an official library. Please be tolerant of areas where the library is still being developed, and — as with all the libraries — feel free to contribute wherever you find issues.</p>
|
||||
|
||||
<p>
|
||||
Huge thanks to official maintainers, [anand2312](https://github.com/anand2312/), [dreinon](https://github.com/dreinon), [J0](https://github.com/j0), and [Leynier](https://github.com/leynier).
|
||||
</p>
|
||||
<p>
|
||||
Shoutout to [timkpaine](https://github.com/timkpaine) for maintaining our Conda libraries as well.
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
id: introduction
|
||||
title: Introduction
|
||||
hideTitle: true
|
||||
---
|
||||
|
||||
<div className="flex items-start gap-6 not-prose" id="introduction">
|
||||
<img src="/docs/img/icons/menu/reference-analytics.svg" className="w-8 h-8 rounded" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl text-scale-1200 m-0">Self-Hosting Analytics</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RefSubLayout.EducationRow>
|
||||
<RefSubLayout.Details>
|
||||
|
||||
The Supabase Analytics server is a Logflare self-hostable instance that manages the ingestion and query pipelines for searching and aggregating structured analytics events.
|
||||
|
||||
When self-hosting the Analytics server, the full logging experience matching that of the Supabase Platform is available in the Studio instance, allowing for an integrated and enhanced development experience.
|
||||
However, it's important to note that certain [differences](#differences) may arise due to the platform's infrastructure.
|
||||
|
||||
<Admonition type="info" label="Logflare Technical Docs">
|
||||
All logflare technical docs are available at
|
||||
[https://docs.logflare.app](https://docs.logflare.app)
|
||||
</Admonition>
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
Logflare currently requires BigQuery usage. You will need to create a Google Cloud project with billing enabled.
|
||||
|
||||
The requirements are as follows after creating the project:
|
||||
|
||||
- Project ID
|
||||
- Project number
|
||||
- A service account key
|
||||
|
||||
#### Setting up BigQuery Service Account
|
||||
|
||||
To ensure that you have sufficient permissions to insert into your Google Cloud BigQuery, ensure that you have created a service account with either:
|
||||
|
||||
- BigQuery Admin role; or
|
||||
- The following permissions:
|
||||
- bigquery.datasets.create
|
||||
- bigquery.datasets.get
|
||||
- bigquery.datasets.getIamPolicy
|
||||
- bigquery.datasets.update
|
||||
- bigquery.jobs.create
|
||||
- bigquery.routines.create
|
||||
- bigquery.routines.update
|
||||
- bigquery.tables.create
|
||||
- bigquery.tables.delete
|
||||
- bigquery.tables.get
|
||||
- bigquery.tables.getData
|
||||
- bigquery.tables.update
|
||||
- bigquery.tables.updateData
|
||||
|
||||
You can create the service account via the web console or `gcloud`, as per the [Google Cloud documentation](https://cloud.google.com/iam/docs/keys-create-delete). In the web console, you can create the key by navigating to IAM > Service Accounts > Actions (dropdown) > Manage Keys
|
||||
|
||||
We recommend setting the BigQuery Admin role, as it simplifies permissions setup.
|
||||
|
||||
#### Downloading the Service Account key
|
||||
|
||||
After the service account is created, you will need to create a key for the service account. This key will sign the JWTs for API requests that the Analytics server makes with BigQuery.
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Using the example [self-hosting stack based on docker-compose](https://github.com/supabase/supabase/tree/master/docker), you include the logging related services using the following command
|
||||
|
||||
You will first need to update the `.env.example` file with the necessary environment variables.
|
||||
|
||||
- `GOOGLE_PROJECT_ID`
|
||||
- `GOOGLE_PROJECT_NUMBER`
|
||||
|
||||
You will need to place your Service Account key in your present working directory with the filename `gcloud.json`.
|
||||
|
||||
Thereafter, you can run the docker-compose commands and include the logging-specific compose file.
|
||||
|
||||
```bash
|
||||
# assuming you clone the supabase/supabase repo.
|
||||
cd docker
|
||||
docker compose -f docker-compose.yml -f docker-compose-logging.yml up
|
||||
```
|
||||
|
||||
This would include two additional docker services to your compose stack: Logflare and Vector.
|
||||
|
||||
<Admonition type="info" label="Self-hosting">
|
||||
Read more about [self-hosting Logflare](https://docs.logflare.app/self-hosting) as your analytics
|
||||
server.
|
||||
</Admonition>
|
||||
|
||||
#### Vector Usage
|
||||
|
||||
In the Docker Compose example, we utilize vector to coordinate the logging pipeline between the services. However, if you peer into the [vector configuration file](https://github.com/supabase/supabase/blob/master/docker/volumes/logs/vector.yml), you will be able to see that Vector sends logs to the Analytics ingestion endpoint.
|
||||
|
||||
If you need to customize the logging pipeline for your own needs, you must ensure that the payloads matches the expected event schema structure. Without the correct structure, it would cause the Studio Logs UI features to break.
|
||||
|
||||
### Standalone Docker Container
|
||||
|
||||
If desired, you can utilize the standalone docker-container. Please refer to the [docker-compose file](https://github.com/supabase/supabase/tree/master/docker/docker-compose-logging.yml) for required docker configuration.
|
||||
|
||||
Additional supplementary technical documentation on self-hosting and using the `supabase/logflare` image for a full Logflare experience is available at the [official Logflare documentation](https://docs.logflare.app/self-hosting/).
|
||||
|
||||
## Differences
|
||||
|
||||
API logs rely on Kong instead of the Supabase Cloud API Gateway. Logs from Kong are not enriched with platform-only data.
|
||||
|
||||
Within the self-hosted setup, all logs are routed to Logflare via Vector. As Kong routes API requests to PostgREST, self-hosted or local deployments will result in Kong request logs instead.
|
||||
This would result in differences in the log event metadata between self-hosted API requests and Supabase Platform requests.
|
||||
|
||||
## BigQuery
|
||||
|
||||
All log event data is stored in and queried from BigQuery. To use the Analytics server with Supabase you'll need a Google Cloud Platform account for access to BigQuery.
|
||||
|
||||
Make sure to set the `GOOGLE_DATASET_ID_APPEND`, `GOOGLE_PROJECT_ID` and `GOOGLE_PROJECT_NUMBER` environment variables.
|
||||
|
||||
Download your Google Cloud API JWT and store it under gcloud.json in your working directory.
|
||||
|
||||
<Admonition>
|
||||
You must also enable billing on your Google Cloud project, as the streaming inserts feature is
|
||||
required.
|
||||
</Admonition>
|
||||
|
||||
## Production Recommendations
|
||||
|
||||
To self-host in a production setting, we recommend performing the following for a better experience.
|
||||
|
||||
### Ensure that Logflare is behind a firewall and restrict all network access to it besides safe requests.
|
||||
|
||||
Self-hosted Logflare has UI authentication disabled and is intended for exposure to the internet. We recommend restricting access to the dashboard, accessible at the `/dashboard` path.
|
||||
If dashboard access is required for managing sources, we recommend having an authentication layer, such as a VPN.
|
||||
|
||||
### Use a different Postgres Database to store Logflare data.
|
||||
|
||||
Logflare requires a Postgres database to function. However, if there is an issue with you self-hosted Postgres service, you would not be able to debug it as it would also bring Logflare down together.
|
||||
|
||||
The self-hosted example is only used as a minimal example on running the entire stack, however it is not recommended to use the same database server for both production and observability.
|
||||
|
||||
</RefSubLayout.Details>
|
||||
|
||||
<RefSubLayout.Examples>
|
||||
|
||||
### Client libraries
|
||||
|
||||
- [JavaScript - Pino Transport](https://github.com/Logflare/pino-logflare)
|
||||
- [Elixir](https://github.com/Logflare/logflare_api_client)
|
||||
- [Elixir - Logger Backend](https://github.com/Logflare/logflare_logger_backend)
|
||||
- [Erlang](https://github.com/Logflare/logflare_erl)
|
||||
- [Erlang - Lager Backend](https://github.com/Logflare/logflare_lager_backend)
|
||||
- [Cloudflare Worker](https://gist.github.com/chasers/c7a220e91820a1084b27fcfdb18ad6bd)
|
||||
|
||||
### Integrations
|
||||
|
||||
- [Fly - Logs](https://github.com/Logflare/fly-log-shipper)
|
||||
- [Vercel Integration - Logs](https://vercel.com/integrations/logflare)
|
||||
- [Cloudflare App - Logs](https://www.cloudflare.com/apps/logflare/install)
|
||||
|
||||
### Additional links
|
||||
|
||||
- [Source code](https://github.com/logflare/logflare)
|
||||
- [OpenAPI docs](https://logflare.app/api/openapi)
|
||||
- [Supabase Acquires Logflare](https://supabase.com/blog/supabase-acquires-logflare)
|
||||
- [Logflare self-hosting docs](https://docs.logflare.app/self-hosting)
|
||||
|
||||
</RefSubLayout.Examples>
|
||||
|
||||
</RefSubLayout.EducationRow>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
id: introduction
|
||||
title: Introduction
|
||||
hideTitle: true
|
||||
---
|
||||
|
||||
<div className="flex items-start gap-6 not-prose" id="introduction">
|
||||
<img src="/docs/img/icons/menu/reference-swift.svg" className="w-8 h-8 rounded" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl text-scale-1200 m-0">Swift Client Library</h1>
|
||||
<h2 className="text-base font-mono text-scale-1100">@supabase-community/supabase-swift</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-xl">
|
||||
This reference documents every object and method available in Supabase's Swift library, [supabase-swift](https://github.com/supabase-community/supabase-swift). You can use supabase-swift to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
|
||||
We also provide a [supabase](https://pub.dev/packages/supabase) package for non-Swift projects.
|
||||
</div>
|
||||
|
||||
<div className="max-w-xl bg-slate-300 px-4 py-2 rounded-md">
|
||||
<p>The Swift client library is created and maintained by the Supabase community, and is not an official library. Please be tolerant of areas where the library is still being developed, and — as with all the libraries — feel free to contribute wherever you find issues.</p>
|
||||
|
||||
<p>
|
||||
Huge thanks to official maintainer, [Maail](https://github.com/maail).
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as _ from 'lodash'
|
||||
import * as fs from 'fs'
|
||||
|
||||
export const slugify = (text: string) => {
|
||||
if (!text) return ''
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
|
||||
@@ -22,6 +22,10 @@ const levelsData = {
|
||||
icon: '/docs/img/icons/menu/database',
|
||||
name: 'Database',
|
||||
},
|
||||
api: {
|
||||
icon: '/docs/img/icons/menu/database',
|
||||
name: 'Serverless APIs',
|
||||
},
|
||||
auth: {
|
||||
icon: '/docs/img/icons/menu/auth',
|
||||
name: 'Auth',
|
||||
@@ -34,6 +38,10 @@ const levelsData = {
|
||||
icon: '/docs/img/icons/menu/realtime',
|
||||
name: 'Realtime',
|
||||
},
|
||||
analytics: {
|
||||
icon: '/docs/img/icons/menu/analytics',
|
||||
name: 'Analytics',
|
||||
},
|
||||
storage: {
|
||||
icon: '/docs/img/icons/menu/storage',
|
||||
name: 'Storage',
|
||||
@@ -82,6 +90,10 @@ const levelsData = {
|
||||
icon: '/docs/img/icons/menu/reference-python',
|
||||
name: 'Python Reference v2.0',
|
||||
},
|
||||
reference_swift_v1: {
|
||||
icon: '/docs/img/icons/menu/reference-swift',
|
||||
name: 'Swift Reference v1.0',
|
||||
},
|
||||
reference_cli: {
|
||||
icon: '/docs/img/icons/menu/reference-cli',
|
||||
name: 'CLI Reference',
|
||||
@@ -102,6 +114,10 @@ const levelsData = {
|
||||
icon: '/docs/img/icons/menu/reference-realtime',
|
||||
name: 'Realtime Server Reference',
|
||||
},
|
||||
reference_self_hosting_analytics: {
|
||||
icon: '/docs/img/icons/menu/reference-analytics',
|
||||
name: 'Analytics Server Reference',
|
||||
},
|
||||
}
|
||||
|
||||
const MobileHeader = memo(function MobileHeader() {
|
||||
@@ -298,20 +314,6 @@ const NavContainer = memo(function NavContainer() {
|
||||
})
|
||||
|
||||
const SiteLayout = ({ children }) => {
|
||||
// const mobileMenuOpen = useMenuMobileOpen()
|
||||
|
||||
useEffect(() => {
|
||||
const key = localStorage.getItem('supabaseDarkMode')
|
||||
if (!key) {
|
||||
// Default to dark mode if no preference config
|
||||
document.documentElement.className = 'dark'
|
||||
document.documentElement.style.colorScheme = 'dark'
|
||||
} else {
|
||||
document.documentElement.className = key === 'true' ? 'dark' : ''
|
||||
document.documentElement.style.colorScheme = key === 'true' ? 'dark' : ''
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
||||
@@ -14,19 +14,8 @@ interface Props {
|
||||
}
|
||||
|
||||
const Layout: FC<Props> = (props: Props) => {
|
||||
// const contentString = renderToString(props.children)
|
||||
const [active, setActive] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const key = localStorage.getItem('supabaseDarkMode')
|
||||
if (!key) {
|
||||
// Default to dark mode if no preference config
|
||||
document.documentElement.className = 'dark'
|
||||
} else {
|
||||
document.documentElement.className = key === 'true' ? 'dark' : ''
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(function () {
|
||||
setActive(true)
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import matter from 'gray-matter'
|
||||
import nonGeneratedReferencePages from 'data/nonGeneratedReferencePages'
|
||||
import { REFERENCES } from 'components/Navigation/Navigation.constants'
|
||||
import { REFERENCES } from '~/components/Navigation/NavigationMenu/NavigationMenu.constants'
|
||||
|
||||
const docsDirectory = process.cwd()
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import specStorageV0 from '~/../../spec/storage_v0_config.yaml' assert { type: '
|
||||
import specRealtimeV0 from '~/../../spec/realtime_v0_config.yaml' assert { type: 'yml' }
|
||||
// @ts-expect-error
|
||||
import specAuthV1 from '~/../../spec/gotrue_v1_config.yaml' assert { type: 'yml' }
|
||||
// @ts-expect-error
|
||||
import specAnalyticsV0 from '~/../../spec/analytics_v0_config.yaml' assert { type: 'yml' }
|
||||
|
||||
function getStorageConfigV0() {
|
||||
return { ...specStorageV0 }
|
||||
@@ -17,4 +19,8 @@ function getAuthConfigV1() {
|
||||
return { ...specAuthV1 }
|
||||
}
|
||||
|
||||
export { getStorageConfigV0, getRealtimeConfigV0, getAuthConfigV1 }
|
||||
function getAnalyticsConfigV0() {
|
||||
return { ...specAnalyticsV0 }
|
||||
}
|
||||
|
||||
export { getStorageConfigV0, getRealtimeConfigV0, getAuthConfigV1, getAnalyticsConfigV0 }
|
||||
@@ -267,6 +267,7 @@ export function gen_v3(spec: OpenAPIV3.Document, dest: string, { apiUrl }: { api
|
||||
}
|
||||
|
||||
const slugify = (text: string) => {
|
||||
if (!text) return ''
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
|
||||
@@ -80,6 +80,16 @@ const nextConfig = {
|
||||
},
|
||||
]
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/docs',
|
||||
basePath: false,
|
||||
permanent: false,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
// next.config.js
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
"gen:supabase-js:v2": "npm-run-all gen:supabase-js:v2:ref",
|
||||
"gen:supabase-js:v2:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v2.yml --output ./docs/reference/javascript/generated",
|
||||
"gen:realtime": "npm-run-all gen:realtime:config",
|
||||
"gen:realtime:config": "ts-node ./generator/index.ts gen --type config --input ../../spec/realtime_v0_config.yaml --output ./docs/reference/realtime/generated/config.mdx"
|
||||
"gen:realtime:config": "ts-node ./generator/index.ts gen --type config --input ../../spec/realtime_v0_config.yaml --output ./docs/reference/realtime/generated/config.mdx",
|
||||
"gen:analytics": "npm-run-all gen:analytics:config gen:analytics:usage",
|
||||
"gen:analytics:config": "ts-node ./generator/index.ts gen --type config --input ../../spec/analytics_v0_config.yaml --output ./docs/reference/analytics/generated/config.mdx",
|
||||
"gen:analytics:usage": "ts-node ./generator/index.ts gen --type api --input ../../spec/transforms/analytics_v0_openapi_deparsed.json --output ./docs/reference/analytics/generated/usage.mdx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@algolia/autocomplete-js": "^1.7.2",
|
||||
@@ -45,9 +48,9 @@
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@next/mdx": "^12.0.4",
|
||||
"@radix-ui/react-accordion": "^1.0.1",
|
||||
"@supabase/auth-helpers-nextjs": "^0.5.4",
|
||||
"@supabase/auth-helpers-nextjs": "^0.5.6",
|
||||
"@supabase/auth-helpers-react": "^0.3.1",
|
||||
"@supabase/supabase-js": "^2.8.0",
|
||||
"@supabase/supabase-js": "^2.13.0",
|
||||
"algoliasearch": "^4.14.2",
|
||||
"babel": "^6.23.0",
|
||||
"clsx": "^1.2.1",
|
||||
@@ -65,6 +68,7 @@
|
||||
"mdast-util-to-markdown": "^1.5.0",
|
||||
"mdast-util-to-string": "^3.1.1",
|
||||
"mdx-mermaid": "2.0.0-rc3",
|
||||
"mermaid": "^10.0.2",
|
||||
"micromark-extension-mdxjs": "^1.0.0",
|
||||
"next": "12.3.2",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
|
||||
+35
-22
@@ -1,19 +1,18 @@
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider } from '@supabase/auth-helpers-react'
|
||||
import { ThemeProvider } from 'common/Providers'
|
||||
import { DefaultSeo } from 'next-seo'
|
||||
import Head from 'next/head'
|
||||
import { AuthProvider, ThemeProvider } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { AppPropsWithLayout } from 'types'
|
||||
import { CommandMenuProvider } from 'ui'
|
||||
import components from '~/components'
|
||||
import Favicons from '~/components/Favicons'
|
||||
import SearchProvider from '~/components/Search/SearchProvider'
|
||||
import SiteLayout from '~/layouts/SiteLayout'
|
||||
import { IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants'
|
||||
import { post } from '~/lib/fetchWrappers'
|
||||
import '../styles/algolia-search.scss'
|
||||
import '../styles/ch.scss'
|
||||
import '../styles/docsearch.scss'
|
||||
import '../styles/main.scss?v=1.0.0'
|
||||
import '../styles/new-docs.scss'
|
||||
import '../styles/prism-okaidia.scss'
|
||||
@@ -25,7 +24,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
IS_PLATFORM || LOCAL_SUPABASE ? createBrowserSupabaseClient() : undefined
|
||||
)
|
||||
|
||||
function telemetry(route: string) {
|
||||
function handlePageTelemetry(route: string) {
|
||||
return post(`https://api.supabase.io/platform/telemetry/page`, {
|
||||
referrer: document.referrer,
|
||||
title: document.title,
|
||||
@@ -38,7 +37,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
/*
|
||||
* handle telemetry
|
||||
*/
|
||||
telemetry(url)
|
||||
handlePageTelemetry(url)
|
||||
/*
|
||||
* handle "scroll to top" behaviour on route change
|
||||
*/
|
||||
@@ -60,30 +59,44 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
}
|
||||
}, [router.events])
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Send page telemetry on first page load
|
||||
*/
|
||||
if (router.isReady) {
|
||||
handlePageTelemetry(router.route)
|
||||
}
|
||||
}, [router.isReady])
|
||||
|
||||
const SITE_TITLE = 'Supabase Documentation'
|
||||
|
||||
const AuthContainer = (props) => {
|
||||
return IS_PLATFORM || LOCAL_SUPABASE ? (
|
||||
<SessionContextProvider supabaseClient={supabase}>
|
||||
<AuthProvider>{props.children}</AuthProvider>
|
||||
</SessionContextProvider>
|
||||
) : (
|
||||
<AuthProvider>{props.children}</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Favicons />
|
||||
{IS_PLATFORM || LOCAL_SUPABASE ? (
|
||||
<SessionContextProvider supabaseClient={supabase}>
|
||||
<ThemeProvider>
|
||||
<SearchProvider>
|
||||
<SiteLayout>
|
||||
<Component {...pageProps} />
|
||||
</SiteLayout>
|
||||
</SearchProvider>
|
||||
</ThemeProvider>
|
||||
</SessionContextProvider>
|
||||
) : (
|
||||
<AuthContainer>
|
||||
<ThemeProvider>
|
||||
<SearchProvider>
|
||||
<CommandMenuProvider
|
||||
site="docs"
|
||||
MarkdownHandler={(props) => (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components} {...props} />
|
||||
)}
|
||||
>
|
||||
<SiteLayout>
|
||||
<Component {...pageProps} />
|
||||
</SiteLayout>
|
||||
</SearchProvider>
|
||||
</CommandMenuProvider>
|
||||
</ThemeProvider>
|
||||
)}
|
||||
</AuthContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Html, Head, Main, NextScript } from 'next/document'
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en" className="dark">
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
|
||||
</Head>
|
||||
<body>
|
||||
<body className="dark">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'api',
|
||||
title: 'Serverless APIs',
|
||||
description: 'Auto-generating and Realtime APIs.',
|
||||
sidebar_label: 'Overview',
|
||||
video: 'https://www.youtube.com/v/rPAJJFdtPw0',
|
||||
}
|
||||
|
||||
Supabase auto-generates three types of API directly from your database schema.
|
||||
|
||||
- REST - connect to your database through a restful interface, directly from the browser.
|
||||
- GraphQL - manipulate your database using a graph-like query language.
|
||||
- Realtime - listen to database changes.
|
||||
|
||||
All the APIs are auto-generated from your database and are designed to get you building as fast as possible, without writing a single line of code.
|
||||
|
||||
You can use them directly from the browser (two-tier architecture), or as a complement to your own API server (three-tier architecture).
|
||||
|
||||
## Features
|
||||
|
||||
- **Instant and auto-generated.** <br />As you update your database the changes are immediately accessible through your API.
|
||||
- **Self documenting.** <br />Supabase generates documentation in the Dashboard which updates as you make database changes.
|
||||
- **Secure.** <br />The API is configured to work with PostgreSQL's Row Level Security, provisioned behind an API gateway with key-auth enabled.
|
||||
- **Fast.** <br />Our benchmarks for basic reads are more than 300% faster than Firebase. The API is a very thin layer on top of Postgres, which does most of the heavy lifting.
|
||||
- **Scalable.** <br />The API can serve thousands of simultaneous requests, and works well for Serverless workloads.
|
||||
|
||||
## REST API [#rest-api-overview]
|
||||
|
||||
Supabase provides a RESTful API using [PostgREST](https://postgrest.org/). This is a very thin API layer on top of Postgres.
|
||||
It provides everything you need from a CRUD API at the URL `https://<project_ref>.supabase.co/rest/v1/`.
|
||||
|
||||
The REST interface is automatically reflected from your database's schema and supports:
|
||||
- Basic CRUD operations (Create/Read/Update/Delete)
|
||||
- Arbitrarily deep relationships among tables/views, functions that return table types can also nest related tables/views.
|
||||
- Works with Postgres Views, Materialized Views and Foreign Tables
|
||||
- Works with Postgres Functions
|
||||
- User defined computed columns and computed relationships
|
||||
- Works with the Postgres security model - including Row Level Security, Roles, and Grants.
|
||||
|
||||
The REST API resolves all requests to a single SQL statement leading to fast response times and high throughput.
|
||||
|
||||
Reference:
|
||||
- [Docs](https://postgrest.org/)
|
||||
- [Source Code](https://github.com/PostgREST/postgrest)
|
||||
|
||||
## GraphQL API [#graphql-api-overview]
|
||||
|
||||
Supabase uses [pg_graphql](https://supabase.github.io/pg_graphql/) to expose a GraphQL API endpoint at `https://<project_ref>.supabase.co/graphql/v1/`.
|
||||
You can introspect and query the GraphQL API of an existing Supabase project within Studio [here](https://app.supabase.com/project/_/api/graphiql),
|
||||
or navigate there manually at `API Docs > GraphQL > GraphiQL`.
|
||||
|
||||
The GraphQL interface is automatically reflected from your database's schema and supports:
|
||||
- Basic CRUD operations (Create/Read/Update/Delete)
|
||||
- Support for Tables, Views, Materialized Views, and Foreign Tables
|
||||
- Arbitrarily deep relationships among tables/views
|
||||
- User defined computed fields
|
||||
- The Postgres security model - including Row Level Security, Roles, and Grants.
|
||||
|
||||
The GraphQL API resolves all requests in a single round-trip leading to fast response times and high throughput.
|
||||
|
||||
Reference:
|
||||
- [Docs](https://supabase.github.io/pg_graphql/)
|
||||
- [Source Code](https://github.com/supabase/pg_graphql)
|
||||
|
||||
## Realtime API [#realtime-api-overview]
|
||||
|
||||
Supabase provides a Realtime API using [Realtime](https://github.com/supabase/realtime). You can use this to listen to database changes over websockets.
|
||||
Realtime leverages PostgreSQL's built-in logical replication. You can manage your Realtime API simply by managing Postgres publications.
|
||||
Go to your project's [Replication section](https://app.supabase.com/project/_/database/replication) to get started.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,70 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'api-keys',
|
||||
title: 'Understanding API Keys',
|
||||
description: 'Securing your Serverless API with Postgres Row Level Security.',
|
||||
}
|
||||
|
||||
Supabase provides two default keys when you create a project: an `anon` key, and a `service_role` key. You can find both keys in the [API Settings](https://app.supabase.com/project/_/settings/api).
|
||||
|
||||
The Serverless APIs are designed to work with Postgres Row Level Security (RLS). These keys both map to Postgres roles. You can find an `anon` user and a `service_role` user in the [Roles](http://app.supabase.com/project/_/database/roles) section of the dashboard.
|
||||
|
||||
The keys are both long-lived JWTs. If you decode these keys, you will see that they contain the "role", an "issued date", and an "expiry date" ~10 years in the future.
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "anon",
|
||||
"iat": 1625137684,
|
||||
"exp": 1940713684
|
||||
}
|
||||
```
|
||||
|
||||
## The `anon` key
|
||||
|
||||
The `anon` key has very few privileges. You can use it in your [RLS policies](/docs/guides/auth/row-level-security) for "anonymous" access. For example, this policy will allow access to the `profiles` table:
|
||||
|
||||
```sql
|
||||
create policy "Allow anonymous access" on profiles to anon for
|
||||
select
|
||||
using (true);
|
||||
```
|
||||
|
||||
And similarity for disallowing access:
|
||||
|
||||
```sql
|
||||
create policy "Disallow anonymous access" on profiles to anon for
|
||||
select
|
||||
using (false);
|
||||
```
|
||||
|
||||
If you are using [Supabase Auth](/docs/guides/auth/overview), then the `anon` role will automatically update to `authenticated` once a user is logged in:
|
||||
|
||||
```sql
|
||||
create policy "Allow access to authenticated users" on profiles to authenticated for
|
||||
select
|
||||
using (true);
|
||||
```
|
||||
|
||||
## The `service_role` key
|
||||
|
||||
The "service_role" is a predefined Postgres role with elevated privileges, designed to perform various administrative and service-related tasks. It can bypass Row Level Security, so it should only be used on a private server.
|
||||
|
||||
<Admonition type="caution">
|
||||
Never expose the `service_role` key in a browser or anywhere where a user can see it.
|
||||
</Admonition>
|
||||
|
||||
A common use case for the `service_role` key is running data analytics jobs on the backend. To support joins on user id, it is often useful to grant the service role read access to `auth.users` table.
|
||||
|
||||
```sql
|
||||
grant
|
||||
select
|
||||
on table auth.users to service_role;
|
||||
```
|
||||
|
||||
We have [partnered with GitHub](https://github.blog/changelog/2022-03-28-supabase-is-now-a-github-secret-scanning-partner/) to scan for Supabase `service_role` keys pushed to public repositories.
|
||||
If they detect any keys with service_role privileges being pushed to GitHub, they will forward the API key to us, so that we can automatically revoke the detected secrets and notify you, protecting your data against malicious actors.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,252 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'creating-routes',
|
||||
title: 'Creating API Routes',
|
||||
description:
|
||||
'API routes are automatically created when you create Postgres Tables, Views, or Functions.',
|
||||
}
|
||||
|
||||
API routes are automatically created when you create Postgres Tables, Views, or Functions.
|
||||
|
||||
## Create a table
|
||||
|
||||
Let's create our first API route by creating a table called `todos` to store tasks.
|
||||
This creates a corresponding route `todos` which can accept `GET`, `POST`, `PATCH`, & `DELETE` requests.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Table editor](https://app.supabase.com/project/_/editor) page in the Dashboard.
|
||||
1. Click **New Table** and create a table with the name `todos`.
|
||||
1. Click **Save**.
|
||||
1. Click **New Column** and create a column with the name `task` and type `text`.
|
||||
1. Click **Save**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Create a table called "todos" with a column to store tasks.
|
||||
create table
|
||||
todos (
|
||||
id bigint generated by default as identity primary key,
|
||||
task text check (char_length(task) > 3)
|
||||
);
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## API URL and Keys
|
||||
|
||||
Every Supabase project has a unique API URL. Your API is secured behind an API gateway which requires an API Key for every request.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings/general) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
The REST API and the GraphQL API are both accessible through this URL:
|
||||
|
||||
- REST: `https://<project_ref>.supabase.co/rest/v1`
|
||||
- GraphQL: `https://<project_ref>.supabase.co/graphql/v1`
|
||||
|
||||
Both of these routes require the `anon` key to be passed through an `apikey` header.
|
||||
|
||||
## Using the API
|
||||
|
||||
### REST API
|
||||
|
||||
You can interact with your API directly via HTTP requests, or you can use the client libraries which we provide.
|
||||
|
||||
Let's see how to make a request to the `todos` table which we created in the first step,
|
||||
using the API URL (`SUPABASE_URL`) and Key (`SUPABASE_ANON_KEY`) we provided:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="javascript"
|
||||
>
|
||||
<TabPanel id="javascript" label="Javascript">
|
||||
|
||||
```javascript
|
||||
// Initialize the JS client
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
|
||||
|
||||
// Make a request
|
||||
const { data: todos, error } = await supabase.from('todos').select('*')
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="curl" label="cURL">
|
||||
|
||||
```bash
|
||||
# Append /rest/v1/ to your URL, and then use the table name as the route
|
||||
curl '<SUPABASE_URL>/rest/v1/todos' \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>"
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
JS Reference: [select()](/docs/reference/javascript/select),
|
||||
[insert()](/docs/reference/javascript/insert),
|
||||
[update()](/docs/reference/javascript/update),
|
||||
[upsert()](/docs/reference/javascript/upsert),
|
||||
[delete()](/docs/reference/javascript/delete),
|
||||
[rpc()](/docs/reference/javascript/rpc) (call Postgres functions).
|
||||
|
||||
### GraphQL API
|
||||
|
||||
You can use any GraphQL client with the Supabase GraphQL API. For our GraphQL example we will use [urql](https://formidable.com/open-source/urql/docs/).
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="javascript"
|
||||
>
|
||||
<TabPanel id="javascript" label="Javascript">
|
||||
|
||||
```javascript
|
||||
import { createClient, useQuery } from 'urql'
|
||||
|
||||
// Prepare API key and Authorization header
|
||||
const headers = {
|
||||
apikey: <SUPABASE_ANON_KEY>,
|
||||
authorization: `Bearer ${<SUPABASE_ANON_KEY>}`,
|
||||
}
|
||||
|
||||
// Create GraphQL client
|
||||
// See: https://formidable.com/open-source/urql/docs/basics/react-preact/#setting-up-the-client
|
||||
const client = createClient({
|
||||
url: '<SUPABASE_URL>/graphql/v1',
|
||||
fetchOptions: function createFetchOptions() {
|
||||
return { headers }
|
||||
},
|
||||
})
|
||||
|
||||
// Prepare our GraphQL query
|
||||
const TodosQuery = `
|
||||
query {
|
||||
todosCollection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Query for the data (React)
|
||||
const [result, reexecuteQuery] = useQuery({
|
||||
query: TodosQuery,
|
||||
})
|
||||
|
||||
// Read the result
|
||||
const { data, fetching, error } = result
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="curl" label="cURL">
|
||||
|
||||
```bash
|
||||
# Append /graphql/v1/ to your URL, and then use the table name as the route
|
||||
curl --request POST '<SUPABASE_URL>/graphql/v1' \
|
||||
-H 'apikey: <SUPABASE_ANON_KEY>' \
|
||||
-H 'Authorization: Bearer <SUPABASE_ANON_KEY>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "query":"{ todos(first: 3) { edges { node { id } } } }" }'
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Realtime API
|
||||
|
||||
By default Realtime is disabled on your database. Let's turn on Realtime for the `todos` table.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Replication** in the sidebar.
|
||||
3. Control which database events are sent by toggling **Insert**, **Update**, and **Delete**.
|
||||
4. Control which tables broadcast changes by selecting **Source** and toggling each table.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-realtime.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
alter
|
||||
publication supabase_realtime add table todos;
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
From the client, we can listen to any new data that is inserted into the `todos` table:
|
||||
|
||||
```javascript
|
||||
// Initialize the JS client
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
|
||||
|
||||
// Create a function to handle inserts
|
||||
const handleInserts = (payload) => {
|
||||
console.log('Change received!', payload)
|
||||
}
|
||||
|
||||
// Listen to inserts
|
||||
const { data: todos, error } = await supabase.from('todos').on('INSERT', handleInserts).subscribe()
|
||||
```
|
||||
|
||||
Use [subscribe()](/docs/reference/javascript/subscribe) to listen to database changes.
|
||||
The Realtime API works through PostgreSQL's replication functionality. Postgres sends database changes to a [publication](/docs/guides/database/replication#publications)
|
||||
called `supabase_realtime`, and by managing this publication you can control which data is broadcast.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,22 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'graphiql',
|
||||
title: 'GraphiQL documentation',
|
||||
description: 'Some docs on graphiql.',
|
||||
video: 'https://www.youtube.com/v/7CqlTU9aOR4',
|
||||
}
|
||||
|
||||
Every Supabase project has a GraphQL Endpoint: `https://<project_ref>.supabase.co/graphql/v1`.
|
||||
|
||||
This endpoint is compatible with any GraphiQL implementation that can pass an `apikey` header.
|
||||
Some suggested applications:
|
||||
|
||||
- [paw.cloud](https://paw.cloud)
|
||||
- [insomnia.rest](https://insomnia.rest)
|
||||
- [postman.com/graphql](https://www.postman.com/graphql/)
|
||||
- Self-hosted GraphiQL: GraphiQL can be served through a simple HTML file. See [this discussion](https://github.com/supabase/supabase/discussions/6144) for more details.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,206 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'joins-and-nested-tables',
|
||||
title: 'Querying Joins and Nested tables',
|
||||
description: 'The Serverless APIs automatically detect relationships between Postgres tables.',
|
||||
}
|
||||
|
||||
The Serverless APIs automatically detect relationships between Postgres tables. Since Postgres is a relational database, this is a very common scenario.
|
||||
|
||||
## One-to-many joins
|
||||
|
||||
Let's use an example database that stores `countries` and `cities`:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="table"
|
||||
>
|
||||
<TabPanel id="table" label="Tables">
|
||||
|
||||
**Countries**
|
||||
|
||||
| `id` | `name` |
|
||||
| ---- | -------------- |
|
||||
| 1 | United Kingdom |
|
||||
| 2 | United States |
|
||||
|
||||
**Cities**
|
||||
|
||||
| `id` | `name` | `country_id` |
|
||||
| ---- | ----------- | ------------ |
|
||||
| 1 | London | 1 |
|
||||
| 2 | Manchester | 1 |
|
||||
| 3 | Los Angeles | 2 |
|
||||
| 4 | New York | 2 |
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="SQL" label="SQL">
|
||||
|
||||
```sql
|
||||
create table
|
||||
countries ("id" serial primary key, "name" text);
|
||||
|
||||
insert into
|
||||
countries (id, name)
|
||||
values
|
||||
(1, 'United Kingdom'),
|
||||
(2, 'United States');
|
||||
|
||||
create table
|
||||
cities (
|
||||
"id" serial primary key,
|
||||
"name" text,
|
||||
"country_id" int references "countries"
|
||||
);
|
||||
|
||||
insert into
|
||||
cities (name, country_id)
|
||||
values
|
||||
('London', 1),
|
||||
('Manchester', 1),
|
||||
('Los Angeles', 2),
|
||||
('New York', 2);
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
The APIs will automatically detect relationships based on the foreign keys:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.from('countries').select(`
|
||||
id,
|
||||
name,
|
||||
cities ( id, name )
|
||||
`)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```dart
|
||||
final data = await supabase.from('todos').select('id, name, cities(id, name)');
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="graphql" label="GraphQL">
|
||||
|
||||
```javascript
|
||||
const Query = `
|
||||
query {
|
||||
countriesCollection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
cities {
|
||||
id,
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="url" label="URL">
|
||||
|
||||
```bash
|
||||
GET https://[REF].supabase.co/rest/v1/countries?select=id,name,cities(id,name)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Many-to-many joins
|
||||
|
||||
The Serverless APIs will detect many-to-many joins. For example, if you have a database which stored teams of users (where each user could below to many teams):
|
||||
|
||||
```sql
|
||||
create table
|
||||
users ("id" serial primary key, "name" text);
|
||||
|
||||
create table
|
||||
teams ("id" serial primary key, "team_name" text);
|
||||
|
||||
create table
|
||||
members (
|
||||
"id" serial primary key,
|
||||
"user_id" int references users,
|
||||
"team_id" int references teams
|
||||
);
|
||||
```
|
||||
|
||||
In these cases you don't need to explicitly define the joining table (members). If we wanted to fetch all the teams and the members in each team:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.from('teams').select(`
|
||||
id,
|
||||
team_name,
|
||||
users ( id, name )
|
||||
`)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```dart
|
||||
final data = await supabase.from('teams').select('id, team_name, users(id, name)');
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="graphql" label="GraphQL">
|
||||
|
||||
```javascript
|
||||
const Query = `
|
||||
query {
|
||||
teamsCollection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
team_name
|
||||
users {
|
||||
id,
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="url" label="URL">
|
||||
|
||||
```bash
|
||||
GET https://[REF].supabase.co/rest/v1/teams?select=id,team_name,users(id,name)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,236 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
title: 'Build an API route in less than 2 minutes.',
|
||||
subtitle: 'Create your first API route by creating a table called `todos` to store tasks.',
|
||||
breadcrumb: 'API Quickstart',
|
||||
}
|
||||
|
||||
Let's create our first REST route which we can query using `cURL` or the browser.
|
||||
|
||||
We'll create a database table called `todos` for storing tasks. This creates a corresponding API route `/rest/v1/todos` which can accept `GET`, `POST`, `PATCH`, & `DELETE` requests.
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details title="Set up a Supabase project with a 'todos' table">
|
||||
|
||||
[Create a new project](https://app.supabase.com) in the Supabase Dashboard.
|
||||
|
||||
After your project is ready, create a table in your Supabase database. You can do this with either the Table interface or the [SQL Editor](https://app.supabase.com/project/_/sql).
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="sql"
|
||||
>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Create a table called "todos"
|
||||
-- with a column to store tasks.
|
||||
create table todos (
|
||||
id serial primary key,
|
||||
task text
|
||||
);
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
|
||||
<StepHikeCompact.Details title="Allow anonymous access">
|
||||
|
||||
Let's turn on Row Level Security for this table and allow anonymous access.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```sql
|
||||
-- Turn on security
|
||||
alter table "todos"
|
||||
enable row level security;
|
||||
|
||||
-- Allow anonymous access
|
||||
create policy "Allow anonymous access"
|
||||
on todos
|
||||
to anon
|
||||
for select
|
||||
using (true);
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Insert some dummy data">
|
||||
|
||||
Now we can add some data to our table which we can access through our API.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```sql
|
||||
insert into todos (task)
|
||||
values
|
||||
('Create tables'),
|
||||
('Enable security'),
|
||||
('Add data'),
|
||||
('Fetch data from the API');
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Fetch the data">
|
||||
|
||||
Find your API URL and Keys in your Dashboard [API Settings](https://app.supabase.com/project/_/settings/api). You can now query your "todos" table by appending `/rest/v1/todos` to the API URL.
|
||||
|
||||
Copy this block of code, substitute `<PROJECT_REF>` and `<ANON_KEY>`, then run it from a terminal.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
curl 'https://<PROJECT_REF>.supabase.co/rest/v1/todos' \
|
||||
-H "apikey: <ANON_KEY>" \
|
||||
-H "Authorization: Bearer <ANON_KEY>"
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
## Bonus
|
||||
|
||||
There are several options for accessing your data:
|
||||
|
||||
### Browser
|
||||
|
||||
You can query the route in your browser, by appending the `anon` key as a query parameter:
|
||||
|
||||
`https://<PROJECT_REF>.supabase.co/rest/v1/users?apikey=<ANON_KEY>`
|
||||
|
||||
### Client libraries
|
||||
|
||||
We provide a numerous [Client Libraries](https://github.com/supabase/supabase#client-libraries).
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.from('todos').select()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```dart
|
||||
final data = await supabase.from('todos').select('*');
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="python" label="Python">
|
||||
|
||||
```python
|
||||
response = supabase.table('todos').select("*").execute()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### GraphQL
|
||||
|
||||
Every table can be accessed through the GraphQL API by switching `/rest/v1` with `/graphql/v1`.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="urql"
|
||||
>
|
||||
<TabPanel id="urql" label="urql">
|
||||
|
||||
```javascript
|
||||
import { createClient, useQuery } from 'urql'
|
||||
|
||||
const URL = '<SUPABASE_URL>/graphql/v1'
|
||||
const ANON_KEY = '<SUPABASE_ANON_KEY>'
|
||||
|
||||
// Prepare API key and Authorization header
|
||||
const headers = {
|
||||
apikey: `${ANON_KEY}`,
|
||||
authorization: `Bearer ${ANON_KEY}`,
|
||||
}
|
||||
|
||||
const client = createClient({
|
||||
url: URL,
|
||||
fetchOptions: function createFetchOptions() {
|
||||
return { headers }
|
||||
},
|
||||
})
|
||||
|
||||
// Prepare our GraphQL query
|
||||
const TodosQuery = `
|
||||
query {
|
||||
todosCollection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
task
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Query for the data (React)
|
||||
const [result, reexecuteQuery] = useQuery({
|
||||
query: TodosQuery,
|
||||
})
|
||||
|
||||
// Read the result
|
||||
const { data, fetching, error } = result
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,26 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'auto-docs',
|
||||
title: 'Auto-generated documentation',
|
||||
description: 'Supabase provides documentation that updates automatically.',
|
||||
}
|
||||
|
||||
Supabase generates documentation in the [Dashboard](https://app.supabase.com) which updates as you make database changes.
|
||||
|
||||
1. Go to the [API](https://app.supabase.com/project/_/api) page in the Dashboard.
|
||||
2. Select any table under **Tables and Views** in the sidebar.
|
||||
3. Switch between the JavaScript and the cURL docs using the tabs.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-docs.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,33 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'client-libs',
|
||||
title: 'Client Libraries',
|
||||
description: 'Supabase provides several client libraries for the REST and Realtime APIs.',
|
||||
video: 'https://www.youtube.com/v/7CqlTU9aOR4',
|
||||
}
|
||||
|
||||
Supabase provides client libraries for the REST and Realtime APIs. Some libraries are officially supported, and some are contributed by the community.
|
||||
|
||||
## Official Libraries
|
||||
|
||||
| `Language` | `Source Code` | `Documentation` |
|
||||
| --------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| Javascript/Typescript | [supabase-js](https://github.com/supabase/supabase-js) | [Docs](https://supabase.com/docs/reference/javascript/introduction) |
|
||||
| Dart/Flutter | [supabase-dart](https://github.com/supabase/supabase-dart) | [Docs](https://supabase.com/docs/reference/dart/introduction) |
|
||||
|
||||
## Community Libraries
|
||||
|
||||
| `Language` | `Source Code` | `Documentation` |
|
||||
| ----------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| C# | [supabase-csharp](https://github.com/supabase-community/supabase-csharp) | [Docs](https://supabase.com/docs/reference/csharp/introduction) |
|
||||
| Go | [supabase-go](https://github.com/supabase-community/supabase-go) | |
|
||||
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | |
|
||||
| Python | [supabase-py](https://github.com/supabase-community/supabase-py) | [Docs](https://supabase.com/docs/reference/python/initializing) |
|
||||
| Ruby | [supabase-rb](https://github.com/supabase-community/supabase-rb) | |
|
||||
| Swift | [supabase-swift](https://github.com/supabase-community/supabase-swift) | |
|
||||
| Godot Engine (GDScript) | [supabase-gdscript](https://github.com/supabase-community/godot-engine.supabase) | |
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
File renamed without changes.
@@ -0,0 +1,66 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'securing-your-api',
|
||||
title: 'Securing your API',
|
||||
description: 'Securing your Serverless API with Postgres Row Level Security.',
|
||||
}
|
||||
|
||||
The Serverless APIs are designed to work with Postgres Row Level Security (RLS). If you use [Supabase Auth](/docs/guides/auth), you can restrict data based on the logged-in user.
|
||||
To control access to your data, you can use [Policies](/docs/guides/auth#policies).
|
||||
|
||||
## Enabling Row Level Security
|
||||
|
||||
If you create a table through the Dashboard, RLS will be enabled by default. This is not the case, however, if you create a table or view using SQL.
|
||||
To enable RLS on any table:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Authentication](https://app.supabase.com/project/_/auth/users) page in the Dashboard.
|
||||
2. Click on **Policies** in the sidebar.
|
||||
3. Select **Enable RLS** to enable Row Level Security.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
alter table
|
||||
todos enable row level security;
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
With RLS enabled, you can create Policies that allow or disallow users to access and update data. We provide a detailed guide for creating Row Level Security Policies in our [Authorization documentation](/docs/guides/auth/row-level-security).
|
||||
|
||||
## Safeguards towards accidental deletes and updates
|
||||
|
||||
By default, all projects have the [safeupdate](https://github.com/eradman/pg-safeupdate) Postgres extension enabled for API queries.
|
||||
This ensures that `delete()` and `update()` requests will fail if there are no filters provided.
|
||||
To confirm that safeupdate is enabled for API queries, run the following query:
|
||||
|
||||
```sql
|
||||
select
|
||||
usename,
|
||||
useconfig
|
||||
from
|
||||
pg_shadow
|
||||
where
|
||||
usename = 'authenticator';
|
||||
```
|
||||
|
||||
The expected value for `useconfig` should be:
|
||||
|
||||
```sql
|
||||
['session_preload_libraries=supautils, safeupdate']
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
File renamed without changes.
@@ -66,7 +66,7 @@ const App = () => (
|
||||
|
||||
### Social Providers
|
||||
|
||||
The Auth component also supports login with [offical social providers](../../auth#providers).
|
||||
The Auth component also supports login with [official social providers](../../auth#providers).
|
||||
|
||||
```js lines=13 title=/src/index.js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
@@ -322,6 +322,8 @@ const App = () => (
|
||||
)
|
||||
```
|
||||
|
||||
Currently, translating error messages (e.g. "Invalid credentials") is not supported. Check [related issue.](https://github.com/supabase/auth-ui/issues/86)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -2,13 +2,15 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'nextjs-server-components',
|
||||
title: 'Supabase Auth with Next.js Server Components',
|
||||
title: 'Supabase Auth with Next.js app directory',
|
||||
description:
|
||||
'Authentication helpers for creating an authenticated Supabase client in Next.js 13 app directory Server Components.',
|
||||
sidebar_label: 'Next.js Server Components',
|
||||
'Authentication helpers for creating an authenticated Supabase client in Next.js 13 app directory Server Components and Route Handlers.',
|
||||
sidebar_label: 'Next.js (app)',
|
||||
}
|
||||
|
||||
This submodule provides experimental convenience helpers for implementing user authentication in Next.js Server Components - the `app` directory. For examples using the `pages` directory check out [Auth Helpers in Next.js](/docs/guides/auth/auth-helpers/nextjs).
|
||||
The Next.js Auth Helpers package configures Supabase Auth to store the user's session in a cookie, rather than `localStorage`. This makes the users's session available server-side - in Server Components and Route Handlers - and is automatically sent along with any requests to Supabase.
|
||||
|
||||
> The `app` directory in Next.js is still in beta and expected to change. For examples using the `pages` directory check out [Auth Helpers in Next.js](/docs/guides/auth/auth-helpers/nextjs).
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
@@ -19,9 +21,7 @@ This submodule provides experimental convenience helpers for implementing user a
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
> For a complete implementation example, check out [this repo](https://github.com/supabase/auth-helpers/tree/main/examples/nextjs-server-components).
|
||||
|
||||
> To learn more about fetching and caching Supabase data with Next.js 13 Server Components, check out our [blog](/blog/fetching-and-caching-supabase-data-in-next-js-server-components) or [live stream](https://www.youtube.com/watch?v=QH0P5xZt5wY).
|
||||
> To learn more about Supabase and the Next.js 13 app directory, check out [this playlist](https://youtube.com/playlist?list=PL5S4mPUpp4OtwG-qCxm8gA_hjaBq0OPdz).
|
||||
|
||||
## Install the Next.js helper library
|
||||
|
||||
@@ -61,9 +61,7 @@ NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
```
|
||||
|
||||
## Creating a Supabase Client
|
||||
|
||||
### Server-side
|
||||
## Configure Middleware
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -73,62 +71,9 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Create a new file at `/utils/supabase-server.js` and populate with the following:
|
||||
Middleware runs immediately before each route in rendered. Next.js only provides read access to headers and cookies in Server Components and Route Handlers, however, Supabase needs to be able to set cookies and headers to refresh expired access tokens. Therefore, you must call the `getSession` function in `middleware.js` in order to use a Supabase client in Server Components or Route Handlers.
|
||||
|
||||
```js title="/utils/supabase-server.js"
|
||||
import { headers, cookies } from 'next/headers'
|
||||
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
export const createClient = () =>
|
||||
createServerComponentSupabaseClient({
|
||||
headers,
|
||||
cookies,
|
||||
})
|
||||
```
|
||||
|
||||
> This needs to export a function, as the headers and cookies are not populated with values until the Server Component is requesting data.
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create a new file at `/utils/supabase-server.ts` and populate with the following:
|
||||
|
||||
```ts title="/utils/supabase-server.ts"
|
||||
import { headers, cookies } from 'next/headers'
|
||||
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
import type { Database } from '../lib/database.types'
|
||||
|
||||
export const createClient = () =>
|
||||
createServerComponentSupabaseClient<Database>({
|
||||
headers,
|
||||
cookies,
|
||||
})
|
||||
```
|
||||
|
||||
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createServerSupabaseClient` to add type support to the Supabase client.
|
||||
|
||||
> This needs to export a function, as the headers and cookies are not populated with values until the Server Component is requesting data.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
This will be used any time we need to create a Supabase client _server-side_ - in a Server Component, for example.
|
||||
|
||||
Next, we need a middleware file to refresh the user's session on navigation.
|
||||
|
||||
> If you were using Middleware prior to 12.2, see the [upgrade guide](https://nextjs.org/docs/messages/middleware-upgrade-guide).
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Create a new `middleware.js` file at the same level as your `app` (in the root or `src` directory) and populate with the following:
|
||||
Create a new `middleware.js` file in the root of your project and populate with the following:
|
||||
|
||||
```jsx title="middleware.js"
|
||||
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
@@ -136,13 +81,8 @@ import { NextResponse } from 'next/server'
|
||||
|
||||
export async function middleware(req) {
|
||||
const res = NextResponse.next()
|
||||
|
||||
const supabase = createMiddlewareSupabaseClient({ req, res })
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
await supabase.auth.getSession()
|
||||
return res
|
||||
}
|
||||
```
|
||||
@@ -151,24 +91,21 @@ export async function middleware(req) {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create a new `middleware.ts` file at the same level as your `app` (in the root or `src` directory) and populate with the following:
|
||||
Middleware runs immediately before each route in rendered. Next.js only provides read access to headers and cookies in Server Components and Route Handlers, however, Supabase needs to be able to set cookies and headers to refresh expired access tokens. Therefore, you must call the `getSession` function in `middleware.ts` in order to use a Supabase client in Server Components or Route Handlers.
|
||||
|
||||
Create a new `middleware.ts` file in the root of your project and populate with the following:
|
||||
|
||||
```tsx title="middleware.ts"
|
||||
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
import type { NextRequest } from 'next/server'
|
||||
import type { Database } from './lib/database.types'
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
const res = NextResponse.next()
|
||||
|
||||
const supabase = createMiddlewareSupabaseClient<Database>({ req, res })
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
await supabase.auth.getSession()
|
||||
return res
|
||||
}
|
||||
```
|
||||
@@ -178,27 +115,9 @@ export async function middleware(req: NextRequest) {
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
We can now use our server-side Supabase client to fetch data in Server Components.
|
||||
## Supabase Provider
|
||||
|
||||
```jsx
|
||||
import 'server-only'
|
||||
|
||||
import { createClient } from '../../utils/supabase-server'
|
||||
|
||||
// do not cache this page
|
||||
export const revalidate = 0
|
||||
|
||||
export default async function ServerComponent() {
|
||||
const supabase = createClient()
|
||||
const { data } = await supabase.from('posts').select('*')
|
||||
|
||||
return <pre>{JSON.stringify({ data }, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
### Client-side
|
||||
|
||||
We still need a Supabase instance client-side for authentication and realtime subscriptions. It is important, when using Supabase client-side, to have a single instance of a client. We can share this singleton instance across our components using providers and React context.
|
||||
All Client Components need to share a single instance of the Supabase client. We can wrap our application in a `<SupabaseProvider />` and use React Context to create a global Supabase instance.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -208,95 +127,32 @@ We still need a Supabase instance client-side for authentication and realtime su
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Create a new file at `/utils/supabase-browser.js` and populate with the following:
|
||||
Create a new file at `/app/supabase-provider.jsx` and populate with the following:
|
||||
|
||||
```js title="/utils/supabase-browser.js"
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
export const createClient = () => createBrowserSupabaseClient()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create a new file at `/utils/supabase-browser.ts` and populate with the following:
|
||||
|
||||
```ts
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { Database } from '../lib/database.types'
|
||||
|
||||
export const createClient = () => createBrowserSupabaseClient<Database>()
|
||||
```
|
||||
|
||||
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createBrowserSupabaseClient` to add type support to the Supabase client.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Next, we need to create a single instance of Supabase to use client-side. Let's create a new Provider for Supabase at `/components/supabase-provider.jsx` and populate with the following:
|
||||
|
||||
```jsx title=components/supabase-provider.jsx
|
||||
```jsx title="app/supabase-provider.jsx"
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
import { createClient } from '../utils/supabase-browser'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
const Context = createContext()
|
||||
const Context = createContext(undefined)
|
||||
|
||||
export default function SupabaseProvider({ children }) {
|
||||
const [supabase] = useState(() => createClient())
|
||||
const [supabase] = useState(() => createBrowserSupabaseClient())
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (session?.access_token !== accessToken) {
|
||||
router.refresh()
|
||||
}
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
router.refresh()
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [accessToken])
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ supabase }}>
|
||||
<>{children}</>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useSupabase = () => useContext(Context)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Next, we need to create a single instance of Supabase to use client-side. Let's create a new Provider for Supabase at `/components/supabase-provider.tsx` and populate with the following:
|
||||
|
||||
```tsx title=components/supabase-provider.tsx
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
import { createClient } from '../utils/supabase-browser'
|
||||
|
||||
import type { SupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import type { Database } from '../lib/database.types'
|
||||
|
||||
type SupabaseContext = {
|
||||
supabase: SupabaseClient<Database>
|
||||
}
|
||||
|
||||
const Context = createContext<SupabaseContext | undefined>(undefined)
|
||||
|
||||
export default function SupabaseProvider({ children }: { children: React.ReactNode }) {
|
||||
const [supabase] = useState(() => createClient())
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}, [router, supabase])
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ supabase }}>
|
||||
@@ -306,12 +162,69 @@ export default function SupabaseProvider({ children }: { children: React.ReactNo
|
||||
}
|
||||
|
||||
export const useSupabase = () => {
|
||||
let context = useContext(Context);
|
||||
const context = useContext(Context)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error("useSupabase must be used inside SupabaseProvider");
|
||||
} else {
|
||||
return context;
|
||||
throw new Error('useSupabase must be used inside SupabaseProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create a new file at `/app/supabase-provider.tsx` and populate with the following:
|
||||
|
||||
```tsx title="app/supabase-provider.tsx"
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import type { SupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
type SupabaseContext = {
|
||||
supabase: SupabaseClient<Database>
|
||||
}
|
||||
|
||||
const Context = createContext<SupabaseContext | undefined>(undefined)
|
||||
|
||||
export default function SupabaseProvider({ children }: { children: React.ReactNode }) {
|
||||
const [supabase] = useState(() => createBrowserSupabaseClient())
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
router.refresh()
|
||||
})
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}, [router, supabase])
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ supabase }}>
|
||||
<>{children}</>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useSupabase = () => {
|
||||
const context = useContext(Context)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useSupabase must be used inside SupabaseProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
```
|
||||
|
||||
@@ -320,16 +233,6 @@ export const useSupabase = () => {
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
We need to set up a listener to fetch fresh data whenever our user logs in or out. For this we need to check whether our client and server sessions match. Let's start by installing the `server-only` package.
|
||||
|
||||
```bash
|
||||
npm install server-only
|
||||
```
|
||||
|
||||
This will ensure that any component that imports this package will be a Server Component, and excluded from the browser bundle.
|
||||
|
||||
Next, let's modify our root layout to fetch the user's session, wrap our application in our Supabase Provider, and pass the server access token as a prop to the `<SupabaseListener />` component (we will create this next).
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
@@ -338,36 +241,22 @@ Next, let's modify our root layout to fetch the user's session, wrap our applica
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title=app/layout.jsx
|
||||
import 'server-only'
|
||||
Modify `layout.jsx` to wrap the application with the `<SupabaseProvider>` component:
|
||||
|
||||
import SupabaseListener from '../components/supabase-listener'
|
||||
import SupabaseProvider from '../components/supabase-provider'
|
||||
```jsx title="app/layout.jsx"
|
||||
import './globals.css'
|
||||
import { createClient } from '../utils/supabase-server'
|
||||
import SupabaseProvider from './supabase-provider'
|
||||
|
||||
// do not cache this layout
|
||||
export const revalidate = 0
|
||||
|
||||
export default async function RootLayout({ children }) {
|
||||
const supabase = createClient()
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
export const metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
{/*
|
||||
<head /> will contain the components returned by the nearest parent
|
||||
head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
|
||||
*/}
|
||||
<head />
|
||||
<body>
|
||||
<SupabaseProvider>
|
||||
<SupabaseListener serverAccessToken={session?.access_token} />
|
||||
{children}
|
||||
</SupabaseProvider>
|
||||
<SupabaseProvider>{children}</SupabaseProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
@@ -378,36 +267,22 @@ export default async function RootLayout({ children }) {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title=app/layout.tsx
|
||||
import 'server-only'
|
||||
Modify `layout.tsx` to wrap the application with the `<SupabaseProvider>` component:
|
||||
|
||||
import SupabaseListener from '../components/supabase-listener'
|
||||
import SupabaseProvider from '../components/supabase-provider'
|
||||
```tsx title="app/layout.tsx"
|
||||
import './globals.css'
|
||||
import { createClient } from '../utils/supabase-server'
|
||||
import SupabaseProvider from './supabase-provider'
|
||||
|
||||
// do not cache this layout
|
||||
export const revalidate = 0
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const supabase = createClient()
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
export const metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
{/*
|
||||
<head /> will contain the components returned by the nearest parent
|
||||
head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
|
||||
*/}
|
||||
<head />
|
||||
<body>
|
||||
<SupabaseProvider>
|
||||
<SupabaseListener serverAccessToken={session?.access_token} />
|
||||
{children}
|
||||
</SupabaseProvider>
|
||||
<SupabaseProvider>{children}</SupabaseProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
@@ -417,7 +292,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
And now create our Supabase listener component that uses the singleton Supabase instance to listen for auth changes.
|
||||
Now any of our Client Components can use the `useSupabase` hook to ensure they are using the same instance of a Supabase client.
|
||||
|
||||
## Creating a Supabase Client
|
||||
|
||||
### Client Components
|
||||
|
||||
While Server Components are great for data fetching, we still need to use Supabase client-side for [authentication](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login.tsx) and [realtime subscriptions](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx).
|
||||
|
||||
As mentioned above, it is important that all Client Components share a single instance of the Supabase client. We can use the `useSupabase` hook we created above to ensure this is the case.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -427,118 +310,24 @@ And now create our Supabase listener component that uses the singleton Supabase
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title="/components/supabase-listener.jsx"
|
||||
```jsx title="app/new-post.jsx"
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useSupabase } from './supabase-provider'
|
||||
|
||||
export default function SupabaseListener({ serverAccessToken }) {
|
||||
export default function NewPost() {
|
||||
const [content, setContent] = useState('')
|
||||
const { supabase } = useSupabase()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (session?.access_token !== serverAccessToken) {
|
||||
router.refresh()
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}, [serverAccessToken, router, supabase])
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title="/components/supabase-listener.tsx"
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
import { useSupabase } from './supabase-provider'
|
||||
|
||||
export default function SupabaseListener({ serverAccessToken }: { serverAccessToken?: string }) {
|
||||
const { supabase } = useSupabase()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (session?.access_token !== serverAccessToken) {
|
||||
router.refresh()
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}, [serverAccessToken, router, supabase])
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
> `use client` tells Next.js that this is a [Client Component](https://beta.nextjs.org/docs/rendering/server-and-client-components#client-components). Only Client Components can use hooks like `useEffect` and `useRouter`.
|
||||
|
||||
The function we pass to `onAuthStateChange` is automatically called by Supabase whenever a user's session changes. This component takes an `serverAccessToken` prop, which is the server's state for our user. If the `serverAccessToken` and the new session's `access_token` do not match then the client and server are out of sync, therefore, we want to reload the active route.
|
||||
|
||||
Now we can use our `useSupabase` hook throughout our client-side components.
|
||||
|
||||
### Authentication
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title="/components/login.jsx"
|
||||
'use client'
|
||||
|
||||
import { useSupabase } from './supabase-provider'
|
||||
|
||||
// Supabase auth needs to be triggered client-side
|
||||
export default function Login() {
|
||||
const { supabase, session } = useSupabase()
|
||||
|
||||
const handleEmailLogin = async () => {
|
||||
await supabase.auth.signInWithPassword({
|
||||
email: 'jon@supabase.com',
|
||||
password: 'password',
|
||||
})
|
||||
}
|
||||
|
||||
const handleGitHubLogin = async () => {
|
||||
await supabase.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
})
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
await supabase.auth.signOut()
|
||||
const handleSave = async () => {
|
||||
const { data } = await supabase.from('posts').insert({ content }).select()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={handleEmailLogin}>Email Login</button>
|
||||
<button onClick={handleGitHubLogin}>GitHub Login</button>
|
||||
<button onClick={handleLogout}>Logout</button>
|
||||
<input onChange={(e) => setContent(e.target.value)} value={content} />
|
||||
<button onClick={handleSave}>Save</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -548,37 +337,24 @@ export default function Login() {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title="/components/login.tsx"
|
||||
```jsx title="app/new-post.tsx"
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useSupabase } from './supabase-provider'
|
||||
|
||||
// Supabase auth needs to be triggered client-side
|
||||
export default function Login() {
|
||||
const { supabase, session } = useSupabase()
|
||||
export default function NewPost() {
|
||||
const [content, setContent] = useState('')
|
||||
const { supabase } = useSupabase()
|
||||
|
||||
const handleEmailLogin = async () => {
|
||||
await supabase.auth.signInWithPassword({
|
||||
email: 'jon@supabase.com',
|
||||
password: 'password',
|
||||
})
|
||||
}
|
||||
|
||||
const handleGitHubLogin = async () => {
|
||||
await supabase.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
})
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
await supabase.auth.signOut()
|
||||
const handleSave = async () => {
|
||||
const { data } = await supabase.from('posts').insert({ content }).select()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={handleEmailLogin}>Email Login</button>
|
||||
<button onClick={handleGitHubLogin}>GitHub Login</button>
|
||||
<button onClick={handleLogout}>Logout</button>
|
||||
<input onChange={(e) => setContent(e.target.value)} value={content} />
|
||||
<button onClick={handleSave}>Save</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -587,11 +363,11 @@ export default function Login() {
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Realtime
|
||||
> check out [this example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/supabase-provider.tsx) for making the user's session available to all Client Components.
|
||||
|
||||
A nice pattern for fetching data server-side and subscribing to changes client-side can be done by combining Server and Client components.
|
||||
### Server Components
|
||||
|
||||
> To receive realtime events, you must [enable replication on your "posts" table in Supabase](https://app.supabase.com/project/_/database/replication).
|
||||
In order to use Supabase in Server Components, you need to have implemented the `middleware.ts` steps above 👆
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -601,57 +377,21 @@ A nice pattern for fetching data server-side and subscribing to changes client-s
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Create a new file at `/app/realtime/posts.jsx` and populate with the following:
|
||||
|
||||
```jsx title="/app/realtime/posts.jsx"
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSupabase } from '../../components/supabase-provider'
|
||||
|
||||
export default function Posts({ serverPosts }) {
|
||||
const [posts, setPosts] = useState(serverPosts)
|
||||
const { supabase } = useSupabase()
|
||||
|
||||
useEffect(() => {
|
||||
setPosts(serverPosts)
|
||||
}, [serverPosts])
|
||||
|
||||
useEffect(() => {
|
||||
const channel = supabase
|
||||
.channel('*')
|
||||
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) =>
|
||||
setPosts((posts) => [...posts, payload.new])
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel)
|
||||
}
|
||||
}, [supabase, setPosts, posts])
|
||||
|
||||
return <pre>{JSON.stringify(posts, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
This can now be used in a Server Component to subscribe to realtime updates.
|
||||
|
||||
Create a new file at `/app/realtime/page.jsx` and populate with the following:
|
||||
|
||||
```jsx title="/app/realtime/page.jsx"
|
||||
import 'server-only'
|
||||
|
||||
import { createClient } from '../../utils/supabase-server'
|
||||
import Posts from './posts'
|
||||
```jsx title="app/page.jsx"
|
||||
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { headers, cookies } from 'next/headers'
|
||||
|
||||
// do not cache this page
|
||||
export const revalidate = 0
|
||||
|
||||
export default async function Realtime() {
|
||||
const supabase = createClient()
|
||||
export default async function ServerComponent() {
|
||||
const supabase = createServerComponentSupabaseClient({
|
||||
headers,
|
||||
cookies,
|
||||
})
|
||||
const { data } = await supabase.from('posts').select('*')
|
||||
|
||||
return <Posts serverPosts={data || []} />
|
||||
return <pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -659,69 +399,91 @@ export default async function Realtime() {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create a new file at `/app/realtime/posts.tsx` and populate with the following:
|
||||
```tsx title="app/page.tsx"
|
||||
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { headers, cookies } from 'next/headers'
|
||||
|
||||
```tsx title="/app/realtime/posts.tsx"
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSupabase } from '../../components/supabase-provider'
|
||||
|
||||
import type { Database } from '../../lib/database.types'
|
||||
|
||||
type Post = Database['public']['Tables']['posts']['Row']
|
||||
|
||||
export default function Posts({ serverPosts }: { serverPosts: Post[] }) {
|
||||
const [posts, setPosts] = useState(serverPosts)
|
||||
const { supabase } = useSupabase()
|
||||
|
||||
useEffect(() => {
|
||||
setPosts(serverPosts)
|
||||
}, [serverPosts])
|
||||
|
||||
useEffect(() => {
|
||||
const channel = supabase
|
||||
.channel('*')
|
||||
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) =>
|
||||
setPosts((posts) => [...posts, payload.new as Post])
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel)
|
||||
}
|
||||
}, [supabase, setPosts, posts])
|
||||
|
||||
return <pre>{JSON.stringify(posts, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createServerSupabaseClient` to add type support to the Supabase client.
|
||||
|
||||
This can now be used in a Server Component to subscribe to realtime updates.
|
||||
|
||||
Create a new file at `/app/realtime/page.tsx` and populate with the following:
|
||||
|
||||
```tsx title="/app/realtime/page.tsx"
|
||||
import 'server-only'
|
||||
|
||||
import { createClient } from '../../utils/supabase-server'
|
||||
import Posts from './posts'
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
// do not cache this page
|
||||
export const revalidate = 0
|
||||
|
||||
export default async function Realtime() {
|
||||
const supabase = createClient()
|
||||
export default async function ServerComponent() {
|
||||
const supabase = createServerComponentSupabaseClient<Database>({
|
||||
headers,
|
||||
cookies,
|
||||
})
|
||||
const { data } = await supabase.from('posts').select('*')
|
||||
|
||||
return <Posts serverPosts={data || []} />
|
||||
return <pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
> check out [this example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/page.tsx) for redirecting unauthenticated users - protected pages.
|
||||
|
||||
### Route Handlers
|
||||
|
||||
In order to use Supabase in Route Handlers, you need to have implemented the `middleware.ts` steps above 👆
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```jsx title="app/api/posts/route.jsx"
|
||||
import { createRouteHandlerSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { headers, cookies } from 'next/headers'
|
||||
|
||||
// do not cache this page
|
||||
export const revalidate = 0
|
||||
|
||||
export async function GET() {
|
||||
const supabase = createRouteHandlerSupabaseClient({
|
||||
headers,
|
||||
cookies,
|
||||
})
|
||||
const { data } = await supabase.from('posts').select('*')
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
```tsx title="app/api/posts/route.tsx"
|
||||
import { createRouteHandlerSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { headers, cookies } from 'next/headers'
|
||||
|
||||
import type { Database } from '@/lib/database.types'
|
||||
|
||||
// do not cache this page
|
||||
export const revalidate = 0
|
||||
|
||||
export async function GET() {
|
||||
const supabase = createRouteHandlerSupabaseClient<Database>({
|
||||
headers,
|
||||
cookies,
|
||||
})
|
||||
const { data } = await supabase.from('posts').select('*')
|
||||
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
> Check out [this repo](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) for a full example including [authentication](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login.tsx), [realtime](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx) and [protected pages](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/page.tsx).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -4,7 +4,7 @@ export const meta = {
|
||||
id: 'nextjs',
|
||||
title: 'Supabase Auth with Next.js',
|
||||
description: 'Authentication helpers for Next.js API routes, middleware, and SSR.',
|
||||
sidebar_label: 'Next.js',
|
||||
sidebar_label: 'Next.js (pages)',
|
||||
}
|
||||
|
||||
This submodule provides convenience helpers for implementing user authentication in Next.js applications.
|
||||
|
||||
@@ -86,7 +86,7 @@ import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
|
||||
return {
|
||||
session: getSession()
|
||||
session: await getSession()
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -142,13 +142,15 @@ We need to create an event listener in the root `+layout.svelte` file in order c
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
$: ({ supabase } = data);
|
||||
$: ({ supabase, session } = data);
|
||||
|
||||
onMount(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth');
|
||||
} = supabase.auth.onAuthStateChange((event, _session) => {
|
||||
if (_session?.expires_at !== session?.expires_at) {
|
||||
invalidate('supabase:auth');
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
@@ -185,26 +187,6 @@ declare global {
|
||||
}
|
||||
```
|
||||
|
||||
### Basic Setup
|
||||
|
||||
You can now determine if a user is authenticated on the client-side by checking that the `session` object in `$page.data` is defined.
|
||||
|
||||
```html
|
||||
<!-- src/lib/components/Header.svelte -->
|
||||
<script lang="ts">
|
||||
import type { Session } from '@supabase/supabase-js';
|
||||
|
||||
export let session: Session | null;
|
||||
</script>
|
||||
|
||||
{#if !session}
|
||||
<h1>I am not logged in</h1>
|
||||
{:else}
|
||||
<h1>Welcome {session.user.email}</h1>
|
||||
<p>I am logged in!</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Client-side data fetching with RLS
|
||||
|
||||
For [row level security](https://supabase.com/docs/guides/auth/row-level-security) to work properly when fetching data client-side, you need to use `supabaseClient` from `PageData` and only run your query once the session is defined client-side:
|
||||
@@ -217,8 +199,8 @@ For [row level security](https://supabase.com/docs/guides/auth/row-level-securit
|
||||
|
||||
let loadedData = [];
|
||||
async function loadData() {
|
||||
const { data } = await data.supabase.from('test').select('*').limit(20);
|
||||
loadedData = data;
|
||||
const { data: result } = await data.supabase.from('test').select('*').limit(20);
|
||||
loadedData = result;
|
||||
}
|
||||
|
||||
$: if (data.session) {
|
||||
@@ -385,7 +367,6 @@ protect multiple routes at once.
|
||||
```ts
|
||||
// src/hooks.server.ts
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
@@ -562,13 +543,15 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
$: ({ supabase } = data);
|
||||
$: ({ supabase, session } = data);
|
||||
|
||||
onMount(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth');
|
||||
} = supabase.auth.onAuthStateChange((event, _session) => {
|
||||
if (_session?.expires_at !== session?.expires_at) {
|
||||
invalidate('supabase:auth')
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Enterprise SSO',
|
||||
title: 'Enterprise Single Sign-On',
|
||||
description: 'Learn about Single Sign-On support in Supabase Auth for enterprise applications',
|
||||
}
|
||||
|
||||
Supabase Auth supports building enterprise applications that require Single Sign-On (SSO) authentication. At this time only [SSO with SAML 2.0](/guides/auth/sso/auth-sso-saml) is supported in an early beta.
|
||||
|
||||
If you are interested in using SAML 2.0 SSO with your Supabase project, please [open a new support ticket](https://app.supabase.com/support/new).
|
||||
Supabase Auth supports building enterprise applications that require Single Sign-On (SSO) authentication [with SAML 2.0](/docs/guides/auth/sso/auth-sso-saml).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'auth-twilio',
|
||||
title: 'Phone Auth with Twilio',
|
||||
@@ -295,6 +294,56 @@ and the response should also be the same as above:
|
||||
|
||||
The user does not have a password therefore will need to sign in via this method each time they want to access your service.
|
||||
|
||||
## WhatsApp OTP Logins
|
||||
|
||||
In some cases, you may wish to use WhatsApp as a delivery channel instead. Here are some examples our users have cited:
|
||||
|
||||
- You want higher deliverability
|
||||
- You wish for a secure channel
|
||||
- Your users mostly use WhatsApp as a messaging platform
|
||||
|
||||
To make use of WhatsApp OTP, please complete the following steps:
|
||||
|
||||
- Go through the [Twilio self sign up guide for WhatsApp](https://www.twilio.com/docs/whatsapp/self-sign-up)
|
||||
- Submit a template via the [Twilio Guide For Submitting WhatsApp templates](https://www.twilio.com/docs/whatsapp/tutorial/send-whatsapp-notification-messages-templates#creating-message-templates-and-submitting-them-for-approval)
|
||||
|
||||
<Admonition type="caution">
|
||||
The message template submitted to Twilio must exactly match the SMS Body entered on the Supabase dashboard.
|
||||
</Admonition>
|
||||
|
||||
The sign in process with WhatsApp is similar to the sign in process for SMS. Do note the additional `whatsapp` parameter added:
|
||||
|
||||
```js
|
||||
const {data, error } = await supabase.auth.signInWithOtp({
|
||||
phone: '+57336567365',
|
||||
options: {
|
||||
channel:'whatsapp'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
You can also sign up with `whatsapp` as a channel:
|
||||
|
||||
```js
|
||||
const {data, error }= await supabase.auth.signUp({
|
||||
phone: '+57336567365',
|
||||
password: 'testsupabasenow',
|
||||
options: {
|
||||
channel:'whatsapp',
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
There is no change in the verification process, you should continue to use the `sms` type for verification
|
||||
```js
|
||||
// After receiving a WhatsApp OTP
|
||||
let { data, error } = await supabase.auth.verifyOtp({
|
||||
phone: '+57336567365',
|
||||
token: '123456',
|
||||
type: 'sms',
|
||||
})
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Twilio Signup](https://www.twilio.com/try-twilio)
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
title: 'Use Supabase Auth with React',
|
||||
subtitle: 'Learn how to Supabase Auth with React.js.',
|
||||
breadcrumb: 'Auth Quickstarts',
|
||||
}
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details title="Create a new Supabase project">
|
||||
|
||||
[Launch a new project](https://app.supabase.com) in the Supabase Dashboard.
|
||||
|
||||
Your new database has a table for storing your users. You can see that this table is currently empty by running some SQL in the [SQL Editor](https://app.supabase.com/project/_/sql).
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```sql SQL_EDITOR
|
||||
select * from auth.users;
|
||||
````
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
|
||||
<StepHikeCompact.Details title="Create a React app">
|
||||
|
||||
Create a React app using the `create-react-app` command.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
npx create-react-app my-app
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Install the Supabase client library">
|
||||
|
||||
The fastest way to get started is to use Supabase's `auth-ui-react` library which provides a convenient interface for working with Supabase Auth from a React app.
|
||||
|
||||
Navigate to the React app and install the Supabase libraries.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
cd my-app && npm install @supabase/supabase-js @supabase/auth-ui-react
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Set up your login component">
|
||||
|
||||
In `index.js`, create a Supabase client using your [Project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
|
||||
|
||||
You can configure the Auth component to display whenever there is no session inside `supabase.auth.getSession()`
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
|
||||
```js src/index.js
|
||||
import './index.css'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
|
||||
const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>')
|
||||
|
||||
export default function App() {
|
||||
const [session, setSession] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session)
|
||||
})
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session)
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [])
|
||||
|
||||
if (!session) {
|
||||
return (<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} />)
|
||||
}
|
||||
else {
|
||||
return (<div>Logged in!</div>)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
<StepHikeCompact.Details title="Start the app">
|
||||
|
||||
Start the app, go to http://localhost:3000 in a browser, and open the browser console and you should be able to log in.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
npm start
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
|
||||
|
||||
export default Page
|
||||
@@ -1,4 +1,5 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { Accordion } from 'ui'
|
||||
|
||||
export const meta = {
|
||||
id: 'server-side-rendering',
|
||||
@@ -55,8 +56,25 @@ like `*` and `**` to allow redirects to different forms of URLs.
|
||||
|
||||
</Admonition>
|
||||
|
||||
These redirect URLs have the following structure:
|
||||
Supabase Auth supports two authentication flows: **Implicit** and **PKCE**. The **PKCE** flow is generally preferred when on the server.
|
||||
It introduces a few additional steps which guard a against replay and URL capture attacks. Unlike the implicit flow, it also allows users to access the
|
||||
`access_token` and `refresh_token` on the server.
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-scale-900 dark:text-white"
|
||||
>
|
||||
<div className="border-b pb-3">
|
||||
<Accordion.Item
|
||||
header={<span className="text-scale-1200 font-bold">Implicit</span>}
|
||||
id={`ssr-implicit-flow`}
|
||||
>
|
||||
|
||||
When using the implicit flow, a redirect URL will be returned with the following structure:
|
||||
```
|
||||
https://yourapp.com/...#access_token=<...>&refresh_token=<...>&...
|
||||
```
|
||||
@@ -77,9 +95,76 @@ your direct control (such as on GitHub Pages or other freemium hosting
|
||||
providers), we want to prevent hosting services from getting access to your
|
||||
user's authorization credentials by default. Even if the server is under your
|
||||
direct control, `GET` requests and their full URLs are often logged. This
|
||||
approach also avoids leaking credentials in request or access logs.
|
||||
|
||||
approach also avoids leaking credentials in request or access logs. If you wish to obtain the
|
||||
`access_token` and `refresh_token` on a server, please consider using the PKCE flow.
|
||||
</Admonition>
|
||||
</Accordion.Item>
|
||||
</div>
|
||||
<div className="border-b pb-3">
|
||||
<Accordion.Item
|
||||
header={<span className="text-scale-1200 font-bold">PKCE</span>}
|
||||
id={`ssr-pkce-flow`}
|
||||
>
|
||||
When using the PKCE flow, a redirect URL will be returned with the following structure:
|
||||
```
|
||||
https://yourapp.com/...?code=<...>
|
||||
```
|
||||
The `code` parameter is commonly known as the Auth Code and can be exchanged for an access token by calling `exchangeCodeForSession(code)`.
|
||||
<Admonition type="info">
|
||||
For security purposes, the code has a validity of 5 minutes and can only be exchanged for an access token once. You
|
||||
will need to restart the authentication flow from scratch if you wish to obtain a new access token.
|
||||
</Admonition>
|
||||
|
||||
As the flow is run server side, `localStorage` may not be available. You may configure the client library to use a custom storage adapter an alternate backing storage such as cookies
|
||||
by setting the `storage` option to an object with the following methods:
|
||||
```js
|
||||
const customStorageAdapter: SupportedStorage = {
|
||||
getItem: (key) => {
|
||||
if (!supportsLocalStorage()) {
|
||||
// Configure alternate storage
|
||||
return null
|
||||
}
|
||||
return globalThis.localStorage.getItem(key)
|
||||
},
|
||||
setItem: (key, value) => {
|
||||
if (!supportsLocalStorage()) {
|
||||
// Configure alternate storage here
|
||||
return
|
||||
}
|
||||
globalThis.localStorage.setItem(key, value)
|
||||
},
|
||||
removeItem: (key) => {
|
||||
if (!supportsLocalStorage()) {
|
||||
// Configure alternate storage here
|
||||
return
|
||||
}
|
||||
globalThis.localStorage.removeItem(key)
|
||||
},
|
||||
}
|
||||
```
|
||||
You may also configure the client library to automatically exchange it for a session after a successful redirect. This can be done by setting the `detectSessionInUrl` option to `true`.
|
||||
|
||||
Putting it all together, your client library initialization may look like this:
|
||||
```js
|
||||
const supabase = createClient(
|
||||
'https://xyzcompany.supabase.co',
|
||||
'public-anon-key',
|
||||
options: {
|
||||
...
|
||||
auth: {
|
||||
...
|
||||
detectSessionInUrl: true,
|
||||
flowType: 'pkce',
|
||||
storage: customStorageAdapter,
|
||||
}
|
||||
...
|
||||
}
|
||||
)
|
||||
```
|
||||
You can read more about the PKCE flow [here](https://oauth.net/2/pkce/)
|
||||
</Accordion.Item>
|
||||
</div>
|
||||
</Accordion>
|
||||
|
||||
## Bringing it together
|
||||
|
||||
@@ -130,6 +215,9 @@ if (refreshToken && accessToken) {
|
||||
await supabase.auth.setSession({
|
||||
refresh_token: refreshToken,
|
||||
access_token: accessToken,
|
||||
{
|
||||
auth: { persistSession: false },
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// make sure you handle this case!
|
||||
@@ -217,3 +305,4 @@ cache keys every hour or less.
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
|
||||
@@ -2,28 +2,37 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'auth-sso-saml',
|
||||
title: 'Single Sign-On with SAML 2.0',
|
||||
description: 'Use Single Sign-On (SSO) authentication with SAML 2.0',
|
||||
title: 'Single Sign-On with SAML 2.0 for Projects',
|
||||
description: 'Use Single Sign-On (SSO) authentication on your project with SAML 2.0',
|
||||
video: 'https://www.youtube.com/v/em1cpOAXknM',
|
||||
}
|
||||
|
||||
Supabase Auth supports enterprise-level Single Sign-On (SSO) for any identity providers compatible with the using the SAML 2.0 protocol.
|
||||
Supabase Auth supports enterprise-level Single Sign-On (SSO) for any identity providers compatible with the using the SAML 2.0 protocol. This is a non-exclusive list of supported identity providers:
|
||||
|
||||
<Admonition type="info">
|
||||
This is an early beta release of these APIs. CLI and Dashboard support for SSO is under development.
|
||||
- Google Workspaces (formerly known as GSuite)
|
||||
- Okta, Auth0
|
||||
- Microsoft Active Directory, Azure Active Directory, Microsoft Entra
|
||||
- PingIdentity
|
||||
- OneLogin
|
||||
|
||||
If you are comfortable using these APIs and would like to try out SSO with SAML 2.0 for your project, please [open a support ticket](https://app.supabase.com/support/new).
|
||||
If you're having issues with identity provider software not on this list, please [open a support ticket](https://app.supabase.com/support/new).
|
||||
|
||||
These APIs are not expected to change before the feature is generally available, but we do reserve the right to modify them. Projects in the Beta will be notified of any changes.
|
||||
## Prerequisites
|
||||
|
||||
</Admonition>
|
||||
This guide requires the use of the [Supabase CLI](/docs/guides/cli). Please make sure you're using version v1.46.4 or higher. You can use `supabase -v` to see the currently installed version.
|
||||
|
||||
You can use the `supabase sso` [subcommands](/docs/reference/cli/supabase-sso) to manage your project's configuration.
|
||||
|
||||
SAML 2.0 support is disabled by default on Supabase projects. You can configure this on the [Auth Providers](https://app.supabase.com/project/_/auth/providers) page on your project.
|
||||
|
||||
Please note that SAML 2.0 support is offered on tiers Pro and above. Check the [Pricing](https://supabase.com/pricing) page for more information.
|
||||
|
||||
## Terminology
|
||||
|
||||
The number of SAML and SSO acronyms can often overwhelming. Here's a glossary which you can refer back to at any time:
|
||||
The number of SAML and SSO acronyms can often be overwhelming. Here's a glossary which you can refer back to at any time:
|
||||
|
||||
- **Identity Provider**, **IdP**, or **IDP**
|
||||
This is software that manages user accounts at a company or organization. It can verify the identity of a user and exchange that information with your Supabase project. Commonly used identity providers are: Microsoft Active Directory (Azure AD, Microsoft Entra), Okta, Google Workspaces (GSuite), PingIdentity, OneLogin, and many others.
|
||||
An identity provider is a service that manages user accounts at a company or organization. It can verify the identity of a user and exchange that information with your Supabase project and other applications. It acts as a single source of truth for user identities and access rights. Commonly used identity providers are: Microsoft Active Directory (Azure AD, Microsoft Entra), Okta, Google Workspaces (GSuite), PingIdentity, OneLogin, and many others. There are also self-hosted and on-prem versions of identity providers, and sometimes they are accessible only by having access to a company VPN or being in a specific building.
|
||||
- **Service Provider**, **SP**
|
||||
This is the software that is asking for user information from an identity provider. In Supabase, this is your project's Auth server.
|
||||
- **Assertion**
|
||||
@@ -47,17 +56,20 @@ The number of SAML and SSO acronyms can often overwhelming. Here's a glossary wh
|
||||
|
||||
Below is information about your project's SAML 2.0 configuration which you can share with the company or organization that you're trying to on-board.
|
||||
|
||||
| Name | Value |
|
||||
| ------------ | --------------------------------------------------------- |
|
||||
| EntityID | `https://<project>.supabase.co/auth/v1/sso/saml/metadata` |
|
||||
| Metadata URL | `https://<project>.supabase.co/auth/v1/sso/saml/metadata` |
|
||||
| ACS URL | `https://<project>.supabase.co/auth/v1/sso/saml/acs` |
|
||||
| SLO URL | `https://<project>.supabase.co/auth/v1/sso/slo` |
|
||||
| NameID | Required `emailAddress` or `persistent` |
|
||||
| Name | Value |
|
||||
| --------------------------- | ----------------------------------------------------------------------- |
|
||||
| EntityID | `https://<project>.supabase.co/auth/v1/sso/saml/metadata` |
|
||||
| Metadata URL | `https://<project>.supabase.co/auth/v1/sso/saml/metadata` |
|
||||
| Metadata URL<br/>(download) | `https://<project>.supabase.co/auth/v1/sso/saml/metadata?download=true` |
|
||||
| ACS URL | `https://<project>.supabase.co/auth/v1/sso/saml/acs` |
|
||||
| SLO URL | `https://<project>.supabase.co/auth/v1/sso/slo` |
|
||||
| NameID | Required `emailAddress` or `persistent` |
|
||||
|
||||
Note that SLO (Single Logout) is not supported at this time with Supabase Auth as it is a rarely supported feature by identity providers. However, the URL is registered and advertised for when this does become available.
|
||||
|
||||
Append `?download=true` to the Metadata URL to get a downloadable Metadata XML file.
|
||||
Append `?download=true` to the Metadata URL to download the Metadata XML file. This is useful in cases where the identity provider requires a file.
|
||||
|
||||
Alternatively, you can use the `supabase sso info --project-ref <your-project>` [command](/docs/reference/cli/supabase-sso-info) to get setup information for your project.
|
||||
|
||||
### User accounts and identities
|
||||
|
||||
@@ -119,18 +131,7 @@ CREATE POLICY "View organization settings."
|
||||
|
||||
## Managing SAML 2.0 connections
|
||||
|
||||
### Prerequisites
|
||||
|
||||
SSO support with SAML 2.0 is in an early beta release. This guide uses the following software which you need to install on your machine to configure your project:
|
||||
|
||||
- [**cURL**](https://curl.se)
|
||||
It is typically pre-installed in macOS and GNU/Linux distributions.
|
||||
- [**jq**](https://stedolan.github.io/jq/)
|
||||
You can install it with `brew install jq` on macOS or using your distribution's package manager.
|
||||
|
||||
You would need access to two keys -- the `anon` and `service_role` key. You can obtain these on the [Project API Keys](https://app.supabase.com/project/_/settings/api) page in the dashboard.
|
||||
|
||||
We publish an [OpenAPI specification](https://github.com/supabase/gotrue/blob/master/openapi.yaml) which you can refer to at any time.
|
||||
Once you've enabled SAML 2.0 support on your project via the [Auth Providers](https://app.supabase.com/project/_/auth/providers) page in the dashboard, you can use the [Supabase CLI](/docs/reference/cli/supabase-sso) to add, update, remove and view information about identity providers.
|
||||
|
||||
### Add a connection
|
||||
|
||||
@@ -151,40 +152,34 @@ Commonly used SAML 2.0 Identity Providers that support Metadata URLs:
|
||||
Commonly used SAML 2.0 Identity Providers that only support Metadata XML files:
|
||||
|
||||
- Google Workspaces (GSuite)
|
||||
- Any self-hosted or on-prem identity provider behind a VPN
|
||||
|
||||
Once you've obtained the SAML 2.0 Metadata XML file or URL you can establish a connection with your project's Supabase Auth server by invoking this API:
|
||||
Once you've obtained the SAML 2.0 Metadata XML file or URL you can [establish a connection](/docs/reference/cli/supabase-sso-add) with your project's Supabase Auth server by running:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'apikey: <anon-jwt>' \
|
||||
-H 'Authorization: Bearer <service_role-jwt>' \
|
||||
--data-binary '@/tmp/body.json' \
|
||||
'https://<project>.supabase.co/auth/v1/admin/sso/providers'
|
||||
```
|
||||
|
||||
To create the `/tmp/body.json` file you can use this:
|
||||
|
||||
```bash
|
||||
jq --null-input \
|
||||
--arg metadata_url "https://..." \
|
||||
'{ "type": "saml", "metadata_url": $metadata_url, "domains": ["company.com"] }' \
|
||||
> /tmp/body.json
|
||||
supabase sso add --type saml --project-ref <your-project> \
|
||||
--metadata-url 'https://company.com/idp/saml/metadata' \
|
||||
--domains company.com
|
||||
```
|
||||
|
||||
If you wish to use a Metadata XML file instead, you can use:
|
||||
|
||||
```bash
|
||||
jq --null-input \
|
||||
-M \
|
||||
--rawfile metadata_file /path/to/metadata.xml \
|
||||
'{ "type": "saml", "metadata_xml": $metadata_file, "domains": ["company.com"] }' \
|
||||
> /tmp/body.json
|
||||
supabase sso add --type saml --project-ref <your-project> \
|
||||
--metadata-file /path/to/saml/metadata.xml \
|
||||
--domains company.com
|
||||
```
|
||||
|
||||
Once you've executed the cURL command with the correct body, you should see details about the registered SAML 2.0 Identity Provider.
|
||||
This command will register a new identity provider with your project's Auth server. When successful, you will see the details of the provider such as it's SAML information and registered domains.
|
||||
|
||||
To initiate a sign-in request from your front-end application you can use:
|
||||
Please note that only persons with write access to the project can register, update or remove identity providers.
|
||||
|
||||
Once you've added an identity provider, users who have access to it can sign in to your application. With SAML 2.0 there are two ways that users can sign in to your project:
|
||||
|
||||
- By signing-in from your application's user interface, commonly known as **SP (Service Provider) Initiated Flow**
|
||||
- By clicking on an icon in the application menu on the company intranet or identity provider page, commonly known as **Identity Provider Initiated (IdP) Flow**
|
||||
|
||||
To initiate a sign-in request from your application's user interface (i.e. the SP Initiated Flow), you can use:
|
||||
|
||||
```typescript
|
||||
supabase.auth.signInWithSSO({
|
||||
@@ -192,11 +187,11 @@ supabase.auth.signInWithSSO({
|
||||
})
|
||||
```
|
||||
|
||||
Which will start the sign-in process using the SSO Identity Provider registered for the `company.com` domain name. If the SSO Identity Provider does not have an associated domain name, you can use `providerId` instead.
|
||||
Calling [`signInWithSSO`](/docs/reference/javascript/auth-signinwithsso) starts the sign-in process using the identity provider registered for the `company.com` domain name. It is not required that identity providers be assigned one or multiple domain names, in which case you can use the provider's unique ID instead.
|
||||
|
||||
### Understanding attribute mappings
|
||||
|
||||
When a user signs in using the SAML 2.0 Single Sign-On protocol, an XML document called the SAML Assertion is exchanged between the Identity Provider and Supabase Auth.
|
||||
When a user signs in using the SAML 2.0 Single Sign-On protocol, an XML document called the SAML Assertion is exchanged between the identity provider and Supabase Auth.
|
||||
|
||||
This assertion contains information about the user's identity and other authentication information, such as:
|
||||
|
||||
@@ -206,9 +201,9 @@ This assertion contains information about the user's identity and other authenti
|
||||
- Department or organization
|
||||
- Other attributes present in the users directory managed by the identity provider
|
||||
|
||||
Other than the unique ID of the user, SAML does not make it mandatory that any other attributes appear in the assertion. Identity Providers are configured about what user information is shared with your project.
|
||||
With exception of the unique user ID, SAML does not require any other attributes in the assertion. Identity providers can be configured so that only select user information is shared with your project.
|
||||
|
||||
Your project can be configured to recognize these attributes and map them into your project's database using a JSON structure. This process is called attribute mapping, and varies according to the configuration of the Identity Provider.
|
||||
Your project can be configured to recognize these attributes and map them into your project's database using a JSON structure. This process is called attribute mapping, and varies according to the configuration of the identity provider.
|
||||
|
||||
For example, the following JSON structure configures attribute mapping for the `email` and `first_name` user identity properties.
|
||||
|
||||
@@ -225,7 +220,14 @@ For example, the following JSON structure configures attribute mapping for the `
|
||||
}
|
||||
```
|
||||
|
||||
You can include this structure in the `POST /auth/v1/admin/sso/providers` call under the `attribute_mapping` property.
|
||||
When creating or updating an identity provider with the [Supabase CLI](/docs/guides/cli) you can include this JSON as a file with the `--attribute-mapping /path/to/attribute/mapping.json` flag.
|
||||
|
||||
For example, to change the attribute mappings to an existing provider you can use:
|
||||
|
||||
```bash
|
||||
supabase sso update <provider-uuid> --project-ref <your-project> \
|
||||
--attribute-mapping /path/to/attribute/mapping.json
|
||||
```
|
||||
|
||||
Given a SAML 2.0 assertion that includes these attributes:
|
||||
|
||||
@@ -268,24 +270,26 @@ Supabase Auth does not require specifying attribute mappings if you only need ac
|
||||
|
||||
At this time it is not possible to have users without an email address, so SAML assertions without one will be rejected.
|
||||
|
||||
Most SAML 2.0 identity providers use LDAP attribute names. However, due to their variability and complexity operators of Identity Providers are able to customize both the `Name` and attribute value that is sent to Supabase Auth in an assertion. Please refer to the identity provider's documentation and contact the operator for details on what attributes are mapped for your project.
|
||||
Most SAML 2.0 identity providers use Lightweight Directory Access Protocol (LDAP) attribute names. However, due to their variability and complexity operators of identity providers are able to customize both the `Name` and attribute value that is sent to Supabase Auth in an assertion. Please refer to the identity provider's documentation and contact the operator for details on what attributes are mapped for your project.
|
||||
|
||||
### Remove a connection
|
||||
|
||||
Once a connection to an identity provider is established, you can remove it by invoking the `DELETE` method on it:
|
||||
Once a connection to an identity provider is established, you can [remove it](/docs/reference/cli/supabase-sso-remove) by running:
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'apikey: <anon-jwt>' \
|
||||
-H 'Authorization: Bearer <service_role-jwt>' \
|
||||
'https://<project>.supabase.co/auth/v1/admin/sso/providers/<provider-uuid>'
|
||||
supabase sso remove <provider-id> --project-ref <your-project>
|
||||
```
|
||||
|
||||
Once a connection is removed, all user accounts from that identity provider will be immediately logged out. User information will remain in the system, but it will no longer be possible for any of those accounts to be accessed in the future, even if you add the connection again.
|
||||
If successful, the details of the removed identity provider will be shown. All user accounts from that identity provider will be immediately logged out. User information will remain in the system, but it will no longer be possible for any of those accounts to be accessed in the future, even if you add the connection again.
|
||||
|
||||
If you need to reassign those user accounts to another identity provider, please [open a support ticket](https://app.supabase.com/support/new).
|
||||
|
||||
A [list of all](/docs/reference/cli/supabase-sso-list) registered identity providers can be displayed by running:
|
||||
|
||||
```bash
|
||||
supabase sso list --project-ref <your-project>
|
||||
```
|
||||
|
||||
### Update a connection
|
||||
|
||||
You may wish to update settings about a connection to a SAML 2.0 identity provider.
|
||||
@@ -293,23 +297,69 @@ You may wish to update settings about a connection to a SAML 2.0 identity provid
|
||||
Commonly this is necessary when:
|
||||
|
||||
- Cryptographic keys are rotated or have expired
|
||||
- Metadata URL has changed, but is the same Identity Provider
|
||||
- Other SAML 2.0 Metadata attributes have changed, but it is still the same Identity Provider
|
||||
- Metadata URL has changed, but is the same identity provider
|
||||
- Other SAML 2.0 Metadata attributes have changed, but it is still the same identity provider
|
||||
- You are updating the domains or attribute mapping
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'apikey: <anon-jwt>' \
|
||||
-H 'Authorization: Bearer <service_role-jwt>' \
|
||||
--data-binary '@/tmp/body.json' \
|
||||
'https://<project>.supabase.co/auth/v1/admin/sso/providers/<provider-uuid>'
|
||||
You can use this command to [update](/docs/reference/cli/supabase-sso-update) the configuration of an identity provider:
|
||||
|
||||
```bash
|
||||
supabase sso update <provider-id> --project-ref <your-project>
|
||||
```
|
||||
|
||||
The request body has the same structure as when you're adding a connection to an Identity Provider.
|
||||
Please use `--help` to see all available flags.
|
||||
|
||||
It is not possible to change the Identity Provider's unique SAML identifier known as `EntityID`. Everything else can be updated. If the SAML `EntityID` of your identity provider has changed, it is regarded as a new identity provider and you will have to register it like a new connection.
|
||||
It is not possible to change the unique SAML identifier of the identity provider, known as `EntityID`. Everything else can be updated. If the SAML `EntityID` of your identity provider has changed, it is regarded as a new identity provider and you will have to register it like a new connection.
|
||||
|
||||
### Retrieving information about a connection
|
||||
|
||||
You can always obtain a [list](/docs/reference/cli/supabase-sso-list) of all registered providers using:
|
||||
|
||||
```bash
|
||||
supabase sso list --project-ref <your-project>
|
||||
```
|
||||
|
||||
This list will only include basic information about each provider. To see [all of the information](/docs/reference/cli/supabase-sso-show) about a provider you can use:
|
||||
|
||||
```bash
|
||||
supabase sso show <provider-id> --project-ref <your-project>
|
||||
```
|
||||
|
||||
You can use the `-o json` flag to output the information as JSON, should you need to. Other formats may be supported, please use `--help` to see all available options.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
### How do I publish my application to an identity provider's marketplace?
|
||||
|
||||
Many cloud-based identity providers offer a marketplace where you can register your application for easy on-boarding with customers. When you use Supabase Auth's SAML 2.0 support you can register your project in any one of these marketplaces.
|
||||
|
||||
Please refer to the relevant documentation for each cloud-based identity provider on how you can do this. Some common marketplaces are:
|
||||
|
||||
- [Okta Integration Network](https://developer.okta.com/docs/guides/build-sso-integration/saml2/main/)
|
||||
- [Azure Active Directory App Gallery](https://learn.microsoft.com/en-us/azure/active-directory-b2c/publish-app-to-azure-ad-app-gallery)
|
||||
- [Google Workspaces Pre-integrated SAML apps catalog](https://support.google.com/a/table/9217027)
|
||||
|
||||
### Why do some users get: SAML Assertion does not contain email address?
|
||||
|
||||
Identity providers do not have to send back and email address for the user, though they often do. Supabase Auth requires that an email address is present.
|
||||
|
||||
The following list of commonly used SAML attribute names is inspected, in order of appearance, to discover the email address in the assertion:
|
||||
|
||||
- `urn:oid:0.9.2342.19200300.100.1.3`
|
||||
- `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`
|
||||
- `http://schemas.xmlsoap.org/claims/EmailAddress`
|
||||
- `mail`
|
||||
- `email`
|
||||
|
||||
Finally if there is no such attribute, it will use the SAML `NameID` value but only if the format is advertised as `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`.
|
||||
|
||||
Should you run into this problem, it is most likely a misconfiguration issue **on the identity provider side.** Please instruct your contact at the company to map the user's email address to one of the above listed attribute names, typically `email`.
|
||||
|
||||
### How do I access the private key used for SAML in my project?
|
||||
|
||||
At this time it is not possible to extract the RSA private key used by your project's Supabase Auth server. This is done to keep the private key as secure as possible, given that SAML does not offer an easy way to rotate keys without disrupting service. (Please use a SAML 2.0 Metadata URL whenever possible for this reason!)
|
||||
|
||||
If you really need access to the key, please [open a support ticket](https://app.supabase.com/support/new) and we'll try to support you as best as possible.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export const meta = {
|
||||
|
||||
Supabase is a flexible platform that lets you decide how you want to build your projects. You can use the Dashboard directly to get up and running quickly, or use a proper local setup. We suggest you work locally and deploy your changes to a linked project on the [Supabase Platform](https://app.supabase.io/).
|
||||
|
||||
Doing things directly on the platform via the [Dashboard](https://app.supabase.io/) is fine when you're getting started, but it's a good idea to move to a proper local workflow before you get too far. Working locally, generating migrations as you change your tables, and appling those migrations to a linked project on the [Platform](https://app.supabase.io/) keeps everything nicely organized as you grow.
|
||||
Doing things directly on the platform via the [Dashboard](https://app.supabase.io/) is fine when you're getting started, but it's a good idea to move to a proper local workflow before you get too far. Working locally, generating migrations as you change your tables, and applying those migrations to a linked project on the [Platform](https://app.supabase.io/) keeps everything nicely organized as you grow.
|
||||
|
||||
## Why develop locally?
|
||||
|
||||
@@ -23,7 +23,7 @@ The Dashboard provides a wide range of features for setting up your project: cre
|
||||
|
||||
4. **Configuration in code**: If you directly change your tables via the Dashboard, none of that gets captured in code. If you follow these local development practices, you'll store all of your table schemas in code.
|
||||
|
||||
5. **Work offline**: Need to work from a train? A plain? An automobile? No problem. Developing your project locally allows you to work offline.
|
||||
5. **Work offline**: Need to work from a train? A plane? An automobile? No problem. Developing your project locally allows you to work offline.
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
@@ -407,12 +407,44 @@ If your project uses Edge Functions, you can deploy these using [`functions depl
|
||||
supabase functions deploy <function_name>
|
||||
```
|
||||
|
||||
### Enabling Local Logging
|
||||
|
||||
Local logs rely on the Supabase Analytics Server. This can be enabled via the CLI configuration, and requires a Google Cloud project and BigQuery access.
|
||||
|
||||
<Admonition type="note">
|
||||
The Google Cloud project must have billing enabled. Read more about this requirement
|
||||
[here](https://supabase.com/docs/reference/self-hosting-analytics/introduction#bigquery).
|
||||
</Admonition>
|
||||
|
||||
Requirements:
|
||||
|
||||
1. Google Cloud project number
|
||||
2. Google Cloud project ID
|
||||
3. Google Cloud Service Account Key, obtained through [Google Cloud IAM dashboard](https://console.cloud.google.com/iam-admin/iam), with BigQuery Admin role assigned to the service account.
|
||||
|
||||
Once you have these 3 items, follow these steps:
|
||||
|
||||
1. Update your project's `config.toml` and fill in the following:
|
||||
|
||||
```
|
||||
[analytics]
|
||||
enabled = true
|
||||
gcp_project_number = "123456"
|
||||
gcp_project_id = "my-project-id"
|
||||
gcp_jwt_path = "supabase/gcloud.json"
|
||||
```
|
||||
|
||||
2. Place your service account key (a JSON file) at the corresponding path and ensure that it is correctly named.
|
||||
|
||||
3. Start your local stack using `supabase start`
|
||||
|
||||
This will switch the logging drivers and will direct logs to the Analytics server. You will be able to view and query your logs via the Studio Logs Explorer and Logs UI.
|
||||
|
||||
## Limitations and considerations
|
||||
|
||||
The local development environment is not as feature-complete as the Supabase Platform. We're working towards parity between the hosted platform and the local environment. Here are some of the differences:
|
||||
|
||||
- The Functions interface is coming soon.
|
||||
- Logs are not supported through the interface, but you can access them through the Docker containers.
|
||||
- You cannot update your project settings in the Dashboard. This must be done using the CLI.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'api',
|
||||
title: 'Serverless APIs',
|
||||
description: 'Auto-generating and Realtime APIs.',
|
||||
sidebar_label: 'Overview',
|
||||
video: 'https://www.youtube.com/v/rPAJJFdtPw0',
|
||||
}
|
||||
|
||||
Supabase auto-generates three types of API directly from your database schema.
|
||||
|
||||
- REST - interact with your database through a restful interface.
|
||||
- Realtime - listen to database changes.
|
||||
- GraphQL - [in beta](https://supabase.com/blog/pg-graphql).
|
||||
|
||||
The APIs are:
|
||||
|
||||
- **Instant and auto-generated.** <br />As you update your database the changes are immediately accessible through your API.
|
||||
- **Self documenting.** <br />Supabase generates documentation in the Dashboard which updates as you make database changes.
|
||||
- **Secure.** <br />The API is configured to work with PostgreSQL's Row Level Security, provisioned behind an API gateway with key-auth enabled.
|
||||
- **Fast.** <br />Our benchmarks for basic reads are more than 300% faster than Firebase. The API is a very thin layer on top of Postgres, which does most of the heavy lifting.
|
||||
- **Scalable.** <br />The API can serve thousands of simultaneous requests, and works well for Serverless workloads.
|
||||
|
||||
## REST API [#rest-api-overview]
|
||||
|
||||
Supabase provides a RESTful API using [PostgREST](https://postgrest.org/). This is a very thin API layer on top of Postgres.
|
||||
It provides everything you need from a CRUD API:
|
||||
|
||||
- Basic CRUD operations
|
||||
- Deeply nested joins, allowing you to fetch data from multiple tables in a single fetch
|
||||
- Works with Postgres Views
|
||||
- Works with Postgres Functions
|
||||
- Works with the Postgres security model - including Row Level Security, Roles, and Grants.
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/rPAJJFdtPw0"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
## GraphQL API [#graphql-api-overview]
|
||||
|
||||
<Admonition type='note'>
|
||||
|
||||
GraphQL is in Beta, and may have breaking changes. It is only available on self-hosted setups and Supabase projects created after 28th March 2022.
|
||||
|
||||
</Admonition>
|
||||
|
||||
GraphQL in Supabase works through [pg_graphql](https://supabase.com/blog/pg-graphql), an open source PostgreSQL extension for GraphQL.
|
||||
|
||||
## Realtime API [#realtime-api-overview]
|
||||
|
||||
Supabase provides a Realtime API using [Realtime](https://github.com/supabase/realtime). You can use this to listen to database changes over websockets.
|
||||
Realtime leverages PostgreSQL's built-in logical replication. You can manage your Realtime API simply by managing Postgres publications.
|
||||
Go to your project's [Replication section](https://app.supabase.com/project/_/database/replication) to get started.
|
||||
|
||||
## Getting started
|
||||
|
||||
All APIs are auto-created from Database tables. After you have added tables or functions to your database, you can use the APIs provided.
|
||||
|
||||
### Creating API Routes
|
||||
|
||||
API routes are automatically created when you create Postgres Tables, Views, or Functions.
|
||||
|
||||
Let's create our first
|
||||
API route by creating a table called `todos` to store tasks.
|
||||
This creates a corresponding route `todos` which can accept `GET`, `POST`, `PATCH`, & `DELETE` requests.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Table editor](https://app.supabase.com/project/_/editor) page in the Dashboard.
|
||||
1. Click **New Table** and create a table with the name `todos`.
|
||||
1. Click **Save**.
|
||||
1. Click **New Column** and create a column with the name `task` and type `text`.
|
||||
1. Click **Save**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Create a table called "todos" with a column to store tasks.
|
||||
|
||||
create table todos (
|
||||
id bigint generated by default as identity primary key,
|
||||
task text check (char_length(task) > 3)
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### API URL and Keys
|
||||
|
||||
Every Supabase project has a unique API URL. Your API is secured behind an API gateway which requires an API Key for every request.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings/general) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
The REST API and the GraphQL API are both accessible through this URL:
|
||||
|
||||
- REST: `https://<project_ref>.supabase.co/rest/v1`
|
||||
- GraphQL: `https://<project_ref>.supabase.co/graphql/v1`
|
||||
|
||||
Both of these routes require the `anon` key to be passed through an `apikey` header.
|
||||
|
||||
#### API Keys
|
||||
|
||||
You are provided with two keys:
|
||||
|
||||
- an `anon` key, which is safe to be used in a browser context.
|
||||
- a `service_role` key, which should only be used on a server. This key can bypass Row Level Security. NEVER use this key in a browser.
|
||||
|
||||
### Accessing the docs in the Dashboard
|
||||
|
||||
#### REST API [#rest-api-dashboard-docs]
|
||||
|
||||
Supabase generates documentation in the [Dashboard](https://app.supabase.com) which updates as you make database changes.
|
||||
Let's view the documentation for a `countries` table which we created in our database.
|
||||
|
||||
1. Go to the [API](https://app.supabase.com/project/_/api) page in the Dashboard.
|
||||
2. Find the `countries` table under **Tables and Views** in the sidebar.
|
||||
3. Switch between the JavaScript and the cURL docs using the tabs.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-docs.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
#### GraphQL
|
||||
|
||||
The GraphQL Endpoint that we provide (`https://<project_ref>.supabase.co/graphql/v1`) is compatible with any GraphiQL implementation that can pass an `apikey` header.
|
||||
Some suggested applications:
|
||||
|
||||
- [paw.cloud](https://paw.cloud)
|
||||
- [insomnia.rest](https://insomnia.rest)
|
||||
- [postman.com/graphql](https://www.postman.com/graphql/)
|
||||
- Self-hosted GraphiQL: GraphiQL can be served through a simple HTML file. See [this discussion](https://github.com/supabase/supabase/discussions/6144) for more details.
|
||||
|
||||
## Using the API
|
||||
|
||||
### REST API
|
||||
|
||||
You can interact with your API directly via HTTP requests, or you can use the client libraries which we provide.
|
||||
|
||||
Let's see how to make a request to the `todos` table which we created in the first step,
|
||||
using the API URL (`SUPABASE_URL`) and Key (`SUPABASE_ANON_KEY`) we provided:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="javascript"
|
||||
>
|
||||
<TabPanel id="javascript" label="Javascript">
|
||||
|
||||
```javascript
|
||||
// Initialize the JS client
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
|
||||
|
||||
// Make a request
|
||||
const { data: todos, error } = await supabase.from('todos').select('*')
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="curl" label="cURL">
|
||||
|
||||
```bash
|
||||
# Append /rest/v1/ to your URL, and then use the table name as the route
|
||||
curl '<SUPABASE_URL>/rest/v1/todos' \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>"
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
JS Reference: [select()](/docs/reference/javascript/select),
|
||||
[insert()](/docs/reference/javascript/insert),
|
||||
[update()](/docs/reference/javascript/update),
|
||||
[upsert()](/docs/reference/javascript/upsert),
|
||||
[delete()](/docs/reference/javascript/delete),
|
||||
[rpc()](/docs/reference/javascript/rpc) (call Postgres functions).
|
||||
|
||||
### GraphQL API
|
||||
|
||||
You can use any GraphQL client with the Supabase GraphQL API. For our GraphQL example we will use [urql](https://formidable.com/open-source/urql/docs/).
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="javascript"
|
||||
>
|
||||
<TabPanel id="javascript" label="Javascript">
|
||||
|
||||
```javascript
|
||||
import { createClient, useQuery } from 'urql'
|
||||
|
||||
// Prepare API key and Authorization header
|
||||
const headers = {
|
||||
apikey: <SUPABASE_ANON_KEY>,
|
||||
authorization: `Bearer ${<SUPABASE_ANON_KEY>}`,
|
||||
}
|
||||
|
||||
// Create GraphQL client
|
||||
// See: https://formidable.com/open-source/urql/docs/basics/react-preact/#setting-up-the-client
|
||||
const client = createClient({
|
||||
url: '<SUPABASE_URL>/graphql/v1',
|
||||
fetchOptions: function createFetchOptions() {
|
||||
return { headers }
|
||||
},
|
||||
})
|
||||
|
||||
// Prepare our GraphQL query
|
||||
const TodosQuery = `
|
||||
query {
|
||||
todosCollection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Query for the data (React)
|
||||
const [result, reexecuteQuery] = useQuery({
|
||||
query: TodosQuery,
|
||||
})
|
||||
|
||||
// Read the result
|
||||
const { data, fetching, error } = result
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="curl" label="cURL">
|
||||
|
||||
```bash
|
||||
# Append /graphql/v1/ to your URL, and then use the table name as the route
|
||||
curl --request POST '<SUPABASE_URL>/graphql/v1' \
|
||||
-H 'apikey: <SUPABASE_ANON_KEY>' \
|
||||
-H 'Authorization: Bearer <SUPABASE_ANON_KEY>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{ "query":"{ todos(first: 3) { edges { node { id } } } }" }'
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Realtime API
|
||||
|
||||
By default Realtime is disabled on your database. Let's turn on Realtime for the `todos` table.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Replication** in the sidebar.
|
||||
3. Control which database events are sent by toggling **Insert**, **Update**, and **Delete**.
|
||||
4. Control which tables broadcast changes by selecting **Source** and toggling each table.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-realtime.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
alter publication supabase_realtime add table todos;
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
From the client, we can listen to any new data that is inserted into the `todos` table:
|
||||
|
||||
```javascript
|
||||
// Initialize the JS client
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
|
||||
|
||||
// Create a function to handle inserts
|
||||
const handleInserts = (payload) => {
|
||||
console.log('Change received!', payload)
|
||||
}
|
||||
|
||||
// Listen to inserts
|
||||
const { data: todos, error } = await supabase.from('todos').on('INSERT', handleInserts).subscribe()
|
||||
```
|
||||
|
||||
Use [subscribe()](/docs/reference/javascript/subscribe) to listen to database changes.
|
||||
The Realtime API works through PostgreSQL's replication functionality. Postgres sends database changes to a [publication](/docs/guides/database/replication#publications)
|
||||
called `supabase_realtime`, and by managing this publication you can control which data is broadcast.
|
||||
|
||||
## API Security
|
||||
|
||||
### Securing your Routes
|
||||
|
||||
Your API is designed to work with Postgres Row Level Security (RLS). If you use Supabase [Auth](/docs/guides/auth), you can restrict data based on the logged-in user.
|
||||
To control access to your data, you can use [Policies](/docs/guides/auth#policies).
|
||||
When you create a table in Postgres, Row Level Security is disabled by default. To enable RLS:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Authentication](https://app.supabase.com/project/_/auth/users) page in the Dashboard.
|
||||
2. Click on **Policies** in the sidebar.
|
||||
3. Select **Enable RLS** to enable Row Level Security.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
alter table todos enable row level security;
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### The `service_role` key
|
||||
|
||||
Never expose the `service_role` key in a browser or anywhere where a user can see it. This Key is designed to bypass Row Level Security - so it should only be used on a private server.
|
||||
|
||||
A common use case for the `service_role` key is to run data analytics jobs on the backend. To support joins on user id, it is often useful to grant the service role read access to `auth.users` table.
|
||||
|
||||
```sql
|
||||
grant select on table auth.users to service_role;
|
||||
```
|
||||
|
||||
We have [partnered with GitHub](https://github.blog/changelog/2022-03-28-supabase-is-now-a-github-secret-scanning-partner/) to scan for Supabase `service_role` keys pushed to public repositories.
|
||||
If they detect any keys with service_role privileges being pushed to GitHub, they will forward the API key to us, so that we can automatically revoke the detected secrets and notify you, protecting your data against malicious actors.
|
||||
|
||||
### Safeguards towards accidental deletes and updates
|
||||
|
||||
For all projects, by default, the Postgres extension [safeupdate](https://github.com/eradman/pg-safeupdate) is enabled for all queries coming from the API.
|
||||
This ensures that any `delete()` or `update()` would fail if there are no accompanying filters provided.
|
||||
To confirm that safeupdate is enabled for queries going through the API of your project, the following query could be run:
|
||||
|
||||
```sql
|
||||
select usename, useconfig from pg_shadow where usename = 'authenticator';
|
||||
```
|
||||
|
||||
The expected value for `useconfig` should be:
|
||||
|
||||
```sql
|
||||
['session_preload_libraries=supautils, safeupdate']
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -140,7 +140,7 @@ returns:
|
||||
This returns the entire array field:
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.from('arraytest').select('textarray[]')
|
||||
const { data, error } = await supabase.from('arraytest').select('textarray')
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
```
|
||||
|
||||
@@ -161,7 +161,7 @@ returns:
|
||||
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [PostgreSQL Arrays](https://www.postgresql.org/docs/12/arrays.html)
|
||||
- [PostgreSQL Arrays](https://www.postgresql.org/docs/15/arrays.html)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -62,8 +62,9 @@ A web interface for connecting to external data is coming to Supabase Studio in
|
||||
|
||||
- [Firebase](https://supabase.github.io/wrappers/firebase/)
|
||||
- [Stripe](https://supabase.github.io/wrappers/stripe/)
|
||||
- [BigQuery](https://supabase.github.io/wrappers/bigquery/)
|
||||
- [AWS S3](https://supabase.github.io/wrappers/s3/)
|
||||
- Airtable - Coming Soon
|
||||
- BigQuery - Coming Soon
|
||||
- Clickhouse - Coming Soon
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -129,7 +129,7 @@ export const examples = [
|
||||
{
|
||||
name: 'Puppeteer',
|
||||
description: `Generate screenshots with Puppeteer.`,
|
||||
href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/puppeteer',
|
||||
href: '/guides/functions/examples/screenshots',
|
||||
},
|
||||
{
|
||||
name: 'Discord Bot',
|
||||
@@ -151,6 +151,11 @@ export const examples = [
|
||||
description: `Build an Edge Functions Counter with Upstash Redis.`,
|
||||
href: '/guides/functions/examples/upstash-redis',
|
||||
},
|
||||
{
|
||||
name: 'Rate Limiting',
|
||||
description: `Rate Limiting Egde Functions with Upstash Redis.`,
|
||||
href: '/guides/functions/examples/rate-limiting',
|
||||
},
|
||||
]
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
|
||||
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'auth',
|
||||
title: 'Auth',
|
||||
title: 'Integrating With Supabase Auth',
|
||||
description: 'Supabase Edge Functions and Auth.',
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'cicd-workflow',
|
||||
title: 'CI / CD Workflow',
|
||||
title: 'Deploying with GitHub Actions',
|
||||
description: 'How to deploy Supabase Edge Functions with a CI / CD pipeline.',
|
||||
video: 'https://www.youtube.com/v/6OMVWiiycLs',
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'examples-postgres-on-the-edge',
|
||||
title: 'Connect to Postgres',
|
||||
title: 'Connecting directly to Postgres',
|
||||
description: 'Connecting to Postgres from Edge Functions.',
|
||||
video: 'https://www.youtube.com/v/cl7EuF1-RsY',
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'functions-cors',
|
||||
title: 'CORS (Cross-Origin Resource Sharing)',
|
||||
title: 'CORS (Cross-Origin Resource Sharing) support for Invoking from the browser',
|
||||
description: 'Add CORS headers to invoke Edge Functions from the browser.',
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'examples-cloudflare-turnstile',
|
||||
title: 'Cloudflare Turnstile',
|
||||
title: 'CAPTCHA support with Cloudflare Turnstile',
|
||||
description: 'Protecting Forms with Cloudflare Turnstile.',
|
||||
video: 'https://www.youtube.com/v/OwW0znboh60',
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'examples-discord-bot',
|
||||
title: 'Discord Bot',
|
||||
title: 'Building a Discord Bot',
|
||||
description: 'Building a Slash Command Discord Bot with Edge Functions.',
|
||||
video: 'https://www.youtube.com/v/J24Bvo_m7DM',
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'examples-openai',
|
||||
title: 'OpenAI',
|
||||
title: 'Generating OpenAI GPT3 completions',
|
||||
description: 'Using OpenAI in Edge Functions.',
|
||||
video: 'https://www.youtube.com/v/29p8kIqyU_Y',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Rate Limiting Edge Functions',
|
||||
description: 'Rate Limiting Edge Functions with Upstash Redis.',
|
||||
}
|
||||
|
||||
<div class="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/o4ooiE-SdUg"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
[Redis](https://redis.io/docs/about/) is an open source (BSD licensed), in-memory data structure store used as a database, cache, message broker, and streaming engine. It is optimized for atomic operations like incrementing a value, for example for a view counter or rate limiting. We can even rate limit based on the user ID from Supabase Auth!
|
||||
|
||||
[Upstash](https://upstash.com/) provides an HTTP/REST based Redis client which is ideal for serverless use-cases and therefore works well with Supabase Edge Functions.
|
||||
|
||||
Find the code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/upstash-redis-ratelimit).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,23 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Taking Screenshots with Puppeteer',
|
||||
description: 'Take screenshots in Edge Functions with Puppeteer and Browserless.io.',
|
||||
}
|
||||
|
||||
<div class="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/Q1nfnQggR4c"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
[Puppeteer](https://pptr.dev/) is a handy tool to programmatically take screenshots and generate PDFs. However, trying to do so in Edge Functions can be challenging due to the size restrictions. Luckily there is a [serverless browser offering available](https://www.browserless.io/) that we can connect to via websockets.
|
||||
|
||||
Find the code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/puppeteer).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -1,10 +1,8 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'examples-stripe-webhooks',
|
||||
title: 'Handling Stripe Webhooks',
|
||||
description: 'Handling signed Stripe Webhooks with Edge Functions.',
|
||||
video: 'https://www.youtube.com/v/6OMVWiiycLs',
|
||||
}
|
||||
|
||||
<div class="video-container">
|
||||
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'examples-telegram-bot',
|
||||
title: 'Telegram Bot',
|
||||
title: 'Building a Telegram Bot',
|
||||
description: 'Building a Telegram Bot with Edge Functions.',
|
||||
video: 'https://www.youtube.com/v/AWfE3a9J_uo',
|
||||
}
|
||||
|
||||
@@ -5,7 +5,16 @@ export const meta = {
|
||||
description: 'Build an Edge Functions Counter with Upstash Redis.',
|
||||
}
|
||||
|
||||
A Redis counter example that stores a [hash](https://redis.io/commands/hincrby/) of function invocation count per region.
|
||||
<div class="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/OPg3_oPZCh0"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
A Redis counter example that stores a [hash](https://redis.io/commands/hincrby/) of function invocation count per region. Find the code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/upstash-redis-counter).
|
||||
|
||||
## Redis database setup
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'functions-debugging',
|
||||
title: 'Globally Distributed Deployments',
|
||||
description: 'We deploy your code throughout the world, close to your users.',
|
||||
}
|
||||
|
||||
We deploy your code throughout the world. Each new request is served from the closest region to your user. Currently located in the following regions:
|
||||
|
||||
1. Taiwan (`asia-east1`)
|
||||
1. Hong Kong (`asia-east2`)
|
||||
1. Tokyo (`asia-northeast1`)
|
||||
1. Osaka (`asia-northeast2`)
|
||||
1. Seoul (`asia-northeast3`)
|
||||
1. Mumbai (`asia-south1`)
|
||||
1. Delhi (`asia-south2`)
|
||||
1. Singapore (`asia-southeast1`)
|
||||
1. Jakarta (`asia-southeast2`)
|
||||
1. Sydney (`australia-southeast1`)
|
||||
1. Melbourne (`australia-southeast2`)
|
||||
1. Warsaw (`europe-central2`)
|
||||
1. Finland (`europe-north1`)
|
||||
1. Belgium (`europe-west1`)
|
||||
1. London (`europe-west2`)
|
||||
1. Frankfurt (`europe-west3`)
|
||||
1. Netherlands (`europe-west4`)
|
||||
1. Zurich (`europe-west6`)
|
||||
1. Milan (`europe-west8`)
|
||||
1. Paris (`europe-west9`)
|
||||
1. Tel Aviv (`me-west1`)
|
||||
1. Madrid (`europe-southwest1`)
|
||||
1. Montréal (`northamerica-northeast1`)
|
||||
1. Toronto (`northamerica-northeast2`)
|
||||
1. São Paulo (`southamerica-east1`)
|
||||
1. Chile (`southamerica-west1`)
|
||||
1. Iowa (`us-central1`)
|
||||
1. South Carolina (`us-east1`)
|
||||
1. North Virginia (`us-east4`)
|
||||
1. Ohio (`us-east5`)
|
||||
1. Texas (`us-south1`)
|
||||
1. Oregon (`us-west1`)
|
||||
1. California (`us-west2`)
|
||||
1. Utah (`us-west3`)
|
||||
1. Nevada (`us-west4`)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -2,8 +2,8 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'functions-import-maps',
|
||||
title: 'Import Maps',
|
||||
description: 'Using Import Maps.',
|
||||
title: 'Managing packages using Import Maps',
|
||||
description: 'Managing packages using Import Maps.',
|
||||
video: 'https://www.youtube.com/v/ILr3cneZuFk',
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'functions-local-development',
|
||||
title: 'Local Development',
|
||||
title: 'Developing Functions locally',
|
||||
description: 'Run your Edge Functions locally.',
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'schedule-functions',
|
||||
title: 'Schedule Edge Functions',
|
||||
title: 'Scheduling Edge Functions',
|
||||
description: 'Schedule Edge Functions with pg_cron.',
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'functions-secrets',
|
||||
title: 'Secrets and Environment Variables',
|
||||
title: 'Managing Secrets and Environment Variables',
|
||||
description: 'Managing secrets and environment variables.',
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'examples-storage-caching',
|
||||
title: 'Caching Images with Supabase Storage CDN',
|
||||
id: 'storage-caching',
|
||||
title: 'Integrating with Supabase Storage',
|
||||
description: 'Integrate Edge Functions with Supabase Storage to cache images on the Edge (CDN).',
|
||||
video: 'https://www.youtube.com/v/wW6L52v9Ldo',
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'functions-debugging',
|
||||
title: 'TypeScript Support',
|
||||
description: 'Using TypeScript in Deno Edge Functions.',
|
||||
}
|
||||
|
||||
One of the benefits of Deno is that it treats TypeScript as a first class
|
||||
language, just like JavaScript or Web Assembly, when running code in Deno. What
|
||||
that means is you can run or import TypeScript without installing anything more
|
||||
than the Deno CLI.
|
||||
|
||||
## How does it work?
|
||||
|
||||
At a high level, Deno converts TypeScript (as well as TSX and JSX) into
|
||||
JavaScript. It does this via a combination of the
|
||||
[TypeScript compiler](https://github.com/microsoft/TypeScript), which we build
|
||||
into Deno, and a Rust library called [swc](https://swc.rs/). When the code has
|
||||
been type checked and transformed, it is stored in a cache, ready for the next
|
||||
run without the need to convert it from its source to JavaScript again.
|
||||
|
||||
## Strict by default
|
||||
|
||||
Deno type checks TypeScript in _strict_ mode by default, and the TypeScript core
|
||||
team recommends _strict_ mode as a sensible default. This mode generally enables
|
||||
features of TypeScript that probably should have been there from the start, but
|
||||
as TypeScript continued to evolve, would be breaking changes for existing code.
|
||||
|
||||
## Mixing JavaScript and TypeScript
|
||||
|
||||
By default, Deno does not type check JavaScript. This can be changed, and is
|
||||
discussed further in [Configuring TypeScript in Deno](./configuration.md). Deno
|
||||
does support JavaScript importing TypeScript and TypeScript importing
|
||||
JavaScript, in complex scenarios.
|
||||
|
||||
An important note though is that when type checking TypeScript, by default Deno
|
||||
will "read" all the JavaScript in order to be able to evaluate how it might have
|
||||
an impact on the TypeScript types. The type checker will do the best it can to
|
||||
figure out what the types are of the JavaScript you import into TypeScript,
|
||||
including reading any JSDoc comments. Details of this are discussed in detail in
|
||||
the [Types and type declarations](./types.md) section.
|
||||
|
||||
## Type resolution
|
||||
|
||||
One of the core design principles of Deno is to avoid non-standard module
|
||||
resolution, and this applies to type resolution as well. If you want to utilize
|
||||
JavaScript that has type definitions (e.g. a `.d.ts` file), you have to
|
||||
explicitly tell Deno about this. The details of how this is accomplished are
|
||||
covered in the [Types and type declarations](https://deno.land/manual/advanced/typescript/types) section.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -36,6 +36,22 @@ export const meta = {
|
||||
|
||||
</div>
|
||||
|
||||
### Use Cases
|
||||
|
||||
<div className="grid lg:grid-cols-12 gap-6 not-prose">
|
||||
{useCases.map((item) => {
|
||||
return (
|
||||
<Link href={`${item.href}`} key={item.title} passHref>
|
||||
<a className={'col-span-4'}>
|
||||
<GlassPanel title={item.title} span="col-span-6" background={false} icon={item.icon}>
|
||||
{item.description}
|
||||
</GlassPanel>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
### Framework Quickstarts
|
||||
|
||||
<div className="grid lg:grid-cols-12 gap-6 not-prose">
|
||||
@@ -84,6 +100,27 @@ export const meta = {
|
||||
})}
|
||||
</div>
|
||||
|
||||
export const useCases = [
|
||||
{
|
||||
title: 'OpenAI Vector Search',
|
||||
href: '/guides/getting-started/openai/vector-search',
|
||||
description: `Build your own custom ChatGPT with Next.js, OpenAI and pg_vector.`,
|
||||
icon: '/docs/img/icons/openai_logo',
|
||||
},
|
||||
{
|
||||
title: 'Subscription Payments (SaaS)',
|
||||
href: 'https://github.com/vercel/nextjs-subscription-payments#nextjs-subscription-payments-starter',
|
||||
description: `Clone, deploy, and fully customize a SaaS subscription application with Next.js.`,
|
||||
icon: '/docs/img/icons/nextjs-icon',
|
||||
},
|
||||
{
|
||||
title: 'Partner Gallery',
|
||||
href: 'https://github.com/supabase-community/partner-gallery-example#supabase-partner-gallery-example',
|
||||
description: `Postgres full-text search, image storage, and more.`,
|
||||
icon: '/docs/img/icons/nextjs-icon',
|
||||
},
|
||||
]
|
||||
|
||||
export const quickstarts = [
|
||||
{
|
||||
title: 'React',
|
||||
@@ -99,6 +136,13 @@ export const quickstarts = [
|
||||
'Learn how to create a Supabase project, add some sample data to your database, and query the data from a NextJS app.',
|
||||
icon: '/docs/img/icons/nextjs-icon',
|
||||
},
|
||||
{
|
||||
title: 'RedwoodJS',
|
||||
href: '/guides/getting-started/quickstarts/redwoodjs',
|
||||
description:
|
||||
'Learn how to create a Supabase project, add some sample data to your database using Prisma migration and seeds, and query the data from a RedwoodJS app.',
|
||||
icon: '/docs/img/icons/redwood-icon',
|
||||
},
|
||||
{
|
||||
title: 'Flutter',
|
||||
href: '/guides/getting-started/quickstarts/flutter',
|
||||
@@ -106,6 +150,27 @@ export const quickstarts = [
|
||||
'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Flutter app.',
|
||||
icon: '/docs/img/icons/flutter-icon',
|
||||
},
|
||||
{
|
||||
title: 'SvelteKit',
|
||||
href: '/guides/getting-started/quickstarts/sveltekit',
|
||||
description:
|
||||
'Learn how to create a Supabase project, add some sample data to your database, and query the data from a SvelteKit app.',
|
||||
icon: '/docs/img/icons/svelte-icon',
|
||||
},
|
||||
{
|
||||
title: 'SolidJS',
|
||||
href: '/guides/getting-started/quickstarts/solidjs',
|
||||
description:
|
||||
'Learn how to create a Supabase project, add some sample data to your database, and query the data from a SolidJS app.',
|
||||
icon: '/docs/img/icons/solidjs-icon',
|
||||
},
|
||||
{
|
||||
title: 'Vue',
|
||||
href: '/guides/getting-started/quickstarts/solidjs',
|
||||
description:
|
||||
'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Vue app.',
|
||||
icon: '/docs/img/icons/vuejs-icon',
|
||||
},
|
||||
]
|
||||
|
||||
export const webapps = [
|
||||
@@ -218,6 +283,12 @@ export const resources = [
|
||||
href: '/guides/getting-started/architecture',
|
||||
description: "An overview of Supabase's architecture and product principles.",
|
||||
},
|
||||
{
|
||||
title: 'Local Development',
|
||||
hasLightIcon: true,
|
||||
href: '/guides/cli/local-development',
|
||||
description: 'Use the Supabase CLI to develop locally and collaborate between teams.',
|
||||
},
|
||||
]
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -115,6 +115,10 @@ Unofficial libraries [supported by the community](https://github.com/supabase-co
|
||||
|
||||
Supabase Storage makes it simple to store and serve files. [Docs](/docs/guides/storage).
|
||||
|
||||
### Resumable uploads
|
||||
|
||||
Upload large files easily using resumable uploads. [Docs](/docs/guides/storage/uploads).
|
||||
|
||||
### Storage CDN
|
||||
|
||||
Cache large files using the Supabase CDN. [Docs](/docs/guides/storage-cdn).
|
||||
@@ -149,35 +153,39 @@ Manage your projects programmatically. [Docs](/docs/reference/api).
|
||||
|
||||
Both Postgres and the Supabase Platform are production-ready. Some tools we offer on top of Postgres are still under development.
|
||||
|
||||
| Product | Feature | Stage |
|
||||
| -------------------------- | ---------------------- | ------- |
|
||||
| Database | Postgres | `GA` |
|
||||
| Database | Triggers | `GA` |
|
||||
| Database | Functions | `GA` |
|
||||
| Database | Extensions | `GA` |
|
||||
| Database | Full Text Search | `GA` |
|
||||
| Database | Webhooks | `alpha` |
|
||||
| Database | Point-in-Time Recovery | `alpha` |
|
||||
| Database | Vault | `alpha` |
|
||||
| Studio | | `GA` |
|
||||
| Realtime | Postgres Changes | `GA` |
|
||||
| Realtime | Broadcast | `beta` |
|
||||
| Realtime | Presence | `beta` |
|
||||
| Storage | | `GA` |
|
||||
| Storage | S3 Backend | `GA` |
|
||||
| Storage | CDN | `GA` |
|
||||
| Storage | Smart CDN | `beta` |
|
||||
| Storage | Image Transformations | `beta` |
|
||||
| Edge Functions | | `beta` |
|
||||
| Auth | OAuth Providers | `beta` |
|
||||
| Auth | Passwordless | `beta` |
|
||||
| Auth | Next.js Auth Helpers | `alpha` |
|
||||
| Auth | SvelteKit Auth Helpers | `alpha` |
|
||||
| Auth | Remix Auth Helpers | `alpha` |
|
||||
| Management API | | `beta` |
|
||||
| CLI | | `beta` |
|
||||
| Client Library: JavaScript | | `GA` |
|
||||
| Client Library: Dart | | `beta` |
|
||||
| Product | Feature | Stage | Available on self-hosted |
|
||||
| -------------------------- | ---------------------- | ------- | ------------------------------------------- |
|
||||
| Database | Postgres | `GA` | ✅ |
|
||||
| Database | Triggers | `GA` | ✅ |
|
||||
| Database | Functions | `GA` | ✅ |
|
||||
| Database | Extensions | `GA` | ✅ |
|
||||
| Database | Full Text Search | `GA` | ✅ |
|
||||
| Database | Webhooks | `GA` | ✅ |
|
||||
| Database | Point-in-Time Recovery | `alpha` | 🚧 [wal-g](https://github.com/wal-g/wal-g) |
|
||||
| Database | Vault | `alpha` | ✅ |
|
||||
| Studio | | `GA` | ✅ |
|
||||
| Realtime | Postgres Changes | `GA` | ✅ |
|
||||
| Realtime | Broadcast | `beta` | ✅ |
|
||||
| Realtime | Presence | `beta` | ✅ |
|
||||
| Storage | | `GA` | ✅ |
|
||||
| Storage | S3 Backend | `GA` | ✅ |
|
||||
| Storage | CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
|
||||
| Storage | Smart CDN | `beta` | 🚧 [Cloudflare](https://www.cloudflare.com) |
|
||||
| Storage | Image Transformations | `beta` | ✅ |
|
||||
| Storage | Resumable Uploads | `beta` | ✅ |
|
||||
| Edge Functions | | `beta` | 🚧 [Deno Deploy](https://deno.com/deploy) |
|
||||
| Auth | OAuth Providers | `beta` | ✅ |
|
||||
| Auth | Passwordless | `beta` | ✅ |
|
||||
| Auth | Next.js Auth Helpers | `alpha` | ✅ |
|
||||
| Auth | SvelteKit Auth Helpers | `alpha` | ✅ |
|
||||
| Auth | Remix Auth Helpers | `alpha` | ✅ |
|
||||
| Management API | | `beta` | N/A |
|
||||
| CLI | | `beta` | N/A |
|
||||
| Client Library: JavaScript | | `GA` | N/A |
|
||||
| Client Library: Dart | | `beta` | N/A |
|
||||
|
||||
- ✅ = Fully Available
|
||||
- 🚧 = Available, but requires external tools or configuration
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
title: 'OpenAI Embeddings & Vector Search',
|
||||
subtitle:
|
||||
'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.',
|
||||
breadcrumb: 'OpenAI',
|
||||
}
|
||||
|
||||
In this tutorial we'll look at how you can build a custom ChatGPT-like search experience for your own knowledge base. See our [Supabase Clippy](https://supabase.com/blog/chatgpt-supabase-docs) blog post for an example of how this will look.
|
||||
|
||||
We assume that you have a Next.js project with a collection of `.mdx` files nested inside your `pages` directory. We will start developing locally with the Supabase CLI and then push our local database changes to our hosted Supabase project.
|
||||
|
||||
<Admonition type="note">
|
||||
You can find the [full example on
|
||||
GitHub](https://github.com/supabase-community/nextjs-openai-doc-search).
|
||||
</Admonition>
|
||||
|
||||
## Create a project
|
||||
|
||||
1. [Create a new project](https://app.supabase.com) in the Supabase Dashboard.
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
## Prepare the database
|
||||
|
||||
Let's prepare the database schema. We can use the "OpenAI Vector Search" quickstart in the [SQL Editor](https://app.supabase.com/project/_/sql), or you can copy/paste the SQL below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **OpenAI Vector Search**.
|
||||
3. Click **Run**.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
|
||||
<StepHikeCompact.Details title="Set up Supabase locally">
|
||||
|
||||
Make sure you have the latest version of the [Supabase CLI installed]().
|
||||
|
||||
Initialize Supabase in the root directory of your app.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Details>
|
||||
|
||||
```bash
|
||||
supabase init
|
||||
```
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
<StepHikeCompact.Details title="Create a migrations file">
|
||||
|
||||
To make changes to our local database, we need to create a new migration. This will create a new `.sql` file in our `supabase/migrations` folder, where we can write SQL that will be applied to our local database when starting Supabase locally.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash
|
||||
supabase migration new init
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Enable the pgvector extension">
|
||||
|
||||
Copy the following SQL line into the newly created migration file to enable the pgvector extension.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```sql
|
||||
-- Enable pgvector extension
|
||||
create extension if not exists vector with schema public;
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Create the database schema">
|
||||
|
||||
Copy these SQL queries to your migration file. It will create two tables in our database schema.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```sql
|
||||
-- Stores the checksum of our pages.
|
||||
-- This ensures that we only regenerate embeddings
|
||||
-- when the page content has changed.
|
||||
create table "public"."nods_page" (
|
||||
id bigserial primary key,
|
||||
parent_page_id bigint references public.nods_page,
|
||||
path text not null unique,
|
||||
checksum text,
|
||||
meta jsonb,
|
||||
type text,
|
||||
source text
|
||||
);
|
||||
alter table "public"."nods_page"
|
||||
enable row level security;
|
||||
|
||||
-- Stores the actual embeddings with some metadata
|
||||
create table "public"."nods_page_section" (
|
||||
id bigserial primary key,
|
||||
page_id bigint not null references public.nods_page on delete cascade,
|
||||
content text,
|
||||
token_count int,
|
||||
embedding vector(1536),
|
||||
slug text,
|
||||
heading text
|
||||
);
|
||||
alter table "public"."nods_page_section"
|
||||
enable row level security;
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Create similarity search database function">
|
||||
|
||||
Anytime the user sends a query, we want to find the content that's relevant to their questions. We can do this using pgvector's similarity search.
|
||||
|
||||
These are quite complex SQL operations, so let's wrap them in database functions that we can call from our frontend using [RPC](https://supabase.com/docs/reference/javascript/rpc).
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```sql
|
||||
-- Create embedding similarity search functions
|
||||
create or replace function match_page_sections(
|
||||
embedding vector(1536),
|
||||
match_threshold float,
|
||||
match_count int,
|
||||
min_content_length int
|
||||
)
|
||||
returns table (
|
||||
id bigint,
|
||||
page_id bigint,
|
||||
slug text,
|
||||
heading text,
|
||||
content text,
|
||||
similarity float
|
||||
)
|
||||
language plpgsql
|
||||
as $$
|
||||
#variable_conflict use_variable
|
||||
begin
|
||||
return query
|
||||
select
|
||||
nods_page_section.id,
|
||||
nods_page_section.page_id,
|
||||
nods_page_section.slug,
|
||||
nods_page_section.heading,
|
||||
nods_page_section.content,
|
||||
(nods_page_section.embedding <#> embedding) * -1 as similarity
|
||||
from nods_page_section
|
||||
|
||||
-- We only care about sections that have a useful amount of content
|
||||
where length(nods_page_section.content) >= min_content_length
|
||||
|
||||
-- The dot product is negative because of a Postgres limitation, so we negate it
|
||||
and (nods_page_section.embedding <#> embedding) * -1 > match_threshold
|
||||
|
||||
-- OpenAI embeddings are normalized to length 1, so
|
||||
-- cosine similarity and dot product will produce the same results.
|
||||
-- Using dot product which can be computed slightly faster.
|
||||
--
|
||||
-- For the different syntaxes, see https://github.com/pgvector/pgvector
|
||||
order by nods_page_section.embedding <#> embedding
|
||||
|
||||
limit match_count;
|
||||
end;
|
||||
$$;
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
<StepHikeCompact.Details title="Start Supabase Locally">
|
||||
|
||||
Start Supabase locally. At this point all files in `supabase/migrations` will be applied to your database and you're ready to go.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash
|
||||
supabase start
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={6}>
|
||||
<StepHikeCompact.Details title="Push changes to your Supabase database">
|
||||
|
||||
Once ready, you can link your local project to your cloud hosted Supabase project and push the local changes to your hosted instance.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash
|
||||
supabase link --project-ref=your-project-ref
|
||||
|
||||
supabase db push
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Pre-process the knowledge base at build time
|
||||
|
||||
With our database set up, we need to process and store all `.mdx` files in the `pages` directory. You can find the full script [here](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/lib/generate-embeddings.ts), or follow the steps below:
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details title="Generate Embeddings">
|
||||
|
||||
Create a new file `lib/generate-embeddings.ts` and copy the code over from [GitHub](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/lib/generate-embeddings.ts).
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash
|
||||
curl \
|
||||
https://raw.githubusercontent.com/supabase-community/nextjs-openai-doc-search/main/lib/generate-embeddings.ts \
|
||||
-o "lib/generate-embeddings.ts"
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
<StepHikeCompact.Details title="Set up environment variables">
|
||||
|
||||
We need some environment variables to run the script. Add them to your `.env` file and make sure your `.env` file is not committed to source control!
|
||||
You can get your local Supabase credentials by running `supabase status`.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```txt
|
||||
NEXT_PUBLIC_SUPABASE_URL=
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||
SUPABASE_SERVICE_ROLE_KEY=
|
||||
|
||||
# Get your key at https://platform.openai.com/account/api-keys
|
||||
OPENAI_KEY=
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Run script at build time">
|
||||
|
||||
Include the script in your `package.json` script commands to enable Vercel to automaticall run it at build time.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "pnpm run embeddings && next build",
|
||||
"start": "next start",
|
||||
"embeddings": "tsx lib/generate-embeddings.ts"
|
||||
},
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
## Create Text Completion with OpenAI API
|
||||
|
||||
Anytime a user asks a question, we need to create an embedding for their question, perform a similarity search, and then send a text completion request to the OpenAI API with the query and then context content merged together into a prompt.
|
||||
|
||||
All of this is glued together in a [Vercel Edge Function](https://vercel.com/docs/concepts/functions/edge-functions), the code for which can be found on [GitHub](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/pages/api/vector-search.ts).
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details title="Create Embedding for Question">
|
||||
|
||||
In order to perform similarity search we need to turn the question into an embedding.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts
|
||||
const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${openAiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'text-embedding-ada-002',
|
||||
input: sanitizedQuery.replaceAll('\n', ' '),
|
||||
}),
|
||||
})
|
||||
|
||||
if (embeddingResponse.status !== 200) {
|
||||
throw new ApplicationError('Failed to create embedding for question', embeddingResponse)
|
||||
}
|
||||
|
||||
const {
|
||||
data: [{ embedding }],
|
||||
} = await embeddingResponse.json()
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
<StepHikeCompact.Details title="Perform similarity search">
|
||||
|
||||
Using the `embeddingResponse` we can now perform similarity search by performing an remote procedure call (RPC) to the database function we created earlier.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts
|
||||
const { error: matchError, data: pageSections } = await supabaseClient.rpc(
|
||||
'match_page_sections',
|
||||
{
|
||||
embedding,
|
||||
match_threshold: 0.78,
|
||||
match_count: 10,
|
||||
min_content_length: 50,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Perform text completion request">
|
||||
|
||||
With the relevant content for the user's question identified, we can now build the prompt and make a text completion request via the OpenAI API.
|
||||
|
||||
If successful, the OpenAI API will respond with a `text/event-stream` response that we can simply forward to the client where we'll process the event stream to smoothly print the answer to the user.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts
|
||||
const prompt = codeBlock`
|
||||
${oneLine`
|
||||
You are a very enthusiastic Supabase representative who loves
|
||||
to help people! Given the following sections from the Supabase
|
||||
documentation, answer the question using only that information,
|
||||
outputted in markdown format. If you are unsure and the answer
|
||||
is not explicitly written in the documentation, say
|
||||
"Sorry, I don't know how to help with that."
|
||||
`}
|
||||
|
||||
Context sections:
|
||||
${contextText}
|
||||
|
||||
Question: """
|
||||
${sanitizedQuery}
|
||||
"""
|
||||
|
||||
Answer as markdown (including related code snippets if available):
|
||||
`
|
||||
|
||||
const completionOptions: CreateCompletionRequest = {
|
||||
model: 'text-davinci-003',
|
||||
prompt,
|
||||
max_tokens: 512,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.openai.com/v1/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${openAiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(completionOptions),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new ApplicationError('Failed to generate completion', error)
|
||||
}
|
||||
|
||||
// Proxy the streamed SSE response from OpenAI
|
||||
return new Response(response.body, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
## Display the answer on the frontend
|
||||
|
||||
In a last step, we need to process the event stream from the OpenAI API and print the answer to the user. The full code for this can be found on [GitHub](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/components/SearchDialog.tsx).
|
||||
|
||||
```tsx
|
||||
const handleConfirm = React.useCallback(
|
||||
async (query: string) => {
|
||||
setAnswer(undefined)
|
||||
setQuestion(query)
|
||||
setSearch('')
|
||||
dispatchPromptData({ index: promptIndex, answer: undefined, query })
|
||||
setHasError(false)
|
||||
setIsLoading(true)
|
||||
|
||||
const eventSource = new SSE(`api/vector-search`, {
|
||||
headers: {
|
||||
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? '',
|
||||
Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
payload: JSON.stringify({ query }),
|
||||
})
|
||||
|
||||
function handleError<T>(err: T) {
|
||||
setIsLoading(false)
|
||||
setHasError(true)
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
eventSource.addEventListener('error', handleError)
|
||||
eventSource.addEventListener('message', (e: any) => {
|
||||
try {
|
||||
setIsLoading(false)
|
||||
|
||||
if (e.data === '[DONE]') {
|
||||
setPromptIndex((x) => {
|
||||
return x + 1
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const completionResponse: CreateCompletionResponse = JSON.parse(e.data)
|
||||
const text = completionResponse.choices[0].text
|
||||
|
||||
setAnswer((answer) => {
|
||||
const currentAnswer = answer ?? ''
|
||||
|
||||
dispatchPromptData({
|
||||
index: promptIndex,
|
||||
answer: currentAnswer + text,
|
||||
})
|
||||
|
||||
return (answer ?? '') + text
|
||||
})
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
})
|
||||
|
||||
eventSource.stream()
|
||||
|
||||
eventSourceRef.current = eventSource
|
||||
|
||||
setIsLoading(true)
|
||||
},
|
||||
[promptIndex, promptData]
|
||||
)
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
Want to learn more about the awesome tech that is powering this?
|
||||
|
||||
- Read about how we built [ChatGPT for the Supabase Docs](https://supabase.com/blog/chatgpt-supabase-docs).
|
||||
- Read the pgvector Docs for [Embeddings and vector similarity](https://supabase.com/docs/guides/database/extensions/pgvector)
|
||||
- Watch Greg's video for a full breakdown:
|
||||
|
||||
<div class="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/Yhtjd7yGGGA"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
|
||||
export default Page
|
||||
@@ -0,0 +1,242 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
title: 'Use Supabase with RedwoodJS',
|
||||
subtitle:
|
||||
'Learn how to create a Supabase project, add some sample data to your database using Prisma migration and seeds, and query the data from a RedwoodJS app.',
|
||||
breadcrumb: 'Framework Quickstarts',
|
||||
}
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details title="Setup your new Supabase Project">
|
||||
[Create a new project](https://app.supabase.com) in the Supabase Dashboard.
|
||||
|
||||
<Admonition type="important">
|
||||
Be sure to make note of the Database Password you used as you will need this later to connect to your database.
|
||||
</Admonition>
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||

|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
<StepHikeCompact.Details title="Gather Database Connection Strings">
|
||||
After your project is ready, gather the following information about your [database connections](https://app.supabase.com/project/_/settings/database):
|
||||
|
||||
* Connection String (port 5432)
|
||||
* Connection Pooling / Connection String (port 6543)
|
||||
|
||||
You will need these to setup environment variables in Step 5.
|
||||
|
||||
<Admonition type="tip">
|
||||
You can copy and paste these connection strings from the Supabase Dashboard when needed in later steps.
|
||||
</Admonition>
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||

|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Create a RedwoodJS app">
|
||||
Create a RedwoodJS app with TypeScript.
|
||||
|
||||
<Admonition type="note">
|
||||
The [`yarn` package manager](https://yarnpkg.com) is required to create a RedwoodJS app. You will use it to run RedwoodJS commands later.
|
||||
|
||||
While TypeScript is recommended, If you want a JavaScript app, omit the `--ts` flag.
|
||||
</Admonition>
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```bash Terminal
|
||||
yarn create redwood-app my-app --ts
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Open your RedwoodJS app in VS Code">
|
||||
You'll develop your app, manage database migrations, and run your app in VS Code.
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```bash Terminal
|
||||
cd my-app
|
||||
code .
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
<StepHikeCompact.Details title="Configure Environment Variables">
|
||||
In your `.env` file, add the following environment variables for your database connection:
|
||||
|
||||
* The `DIRECT_URL` should use the `Connection String` from your Supabase project. Hint: the port is `5432`.
|
||||
|
||||
* The `DATABASE_URL` should use the `Connection Pooling / Connection String` from your Supabase project. Hint: the port is `6432`.
|
||||
|
||||
<Admonition type="note">
|
||||
Be sure to append `?pgbouncer=true` to the end of the `DATABASE_URL` connection string.
|
||||
Also, replace `[YOUR-PASSWORD]` and `[YOUR-PROJECT-REF]` with the password you used when creating your Supabase project and the project reference from the URL in your browser.
|
||||
</Admonition>
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```bash terminal
|
||||
# .env
|
||||
# PostgreSQL connection string used for migrations
|
||||
DIRECT_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"
|
||||
# PostgreSQL connection string with pgBouncer config — used by Prisma Client
|
||||
DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true"
|
||||
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={6}>
|
||||
<StepHikeCompact.Details title="Update your Prisma Schema">
|
||||
By default, RedwoodJS ships with a SQLite database, but we want to use PostgreSQL.
|
||||
|
||||
Update your Prisma schema file `api/db/schema.prisma` to use your Supabase PostgreSQL database connection environment variables you setup in Step 5.
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```file="api/prisma/schema.prisma" title="api/prisma/schema.prisma"
|
||||
// api/db/schema.prisma
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
}
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={7}>
|
||||
<StepHikeCompact.Details title="Create the Country model and apply a schema migration">
|
||||
Create the Country model in `api/db/schema.prisma` and then run `yarn rw prisma migrate dev` from your terminal to apply the migration.
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```js
|
||||
// api/db/schema.prisma
|
||||
|
||||
model Country {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
}
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={8}>
|
||||
<StepHikeCompact.Details title="Update seed script">
|
||||
Let's seed the database with a few countries.
|
||||
|
||||
Update the file `scripts/seeds.ts` to contain the following code:
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts
|
||||
// scripts/seed.ts
|
||||
|
||||
import type { Prisma } from '@prisma/client'
|
||||
import { db } from 'api/src/lib/db'
|
||||
|
||||
export default async () => {
|
||||
try {
|
||||
const data: Prisma.CountryCreateArgs['data'][] = [
|
||||
{ name: 'United States' },
|
||||
{ name: 'Canada' },
|
||||
{ name: 'Mexico' },
|
||||
]
|
||||
|
||||
console.log('Seeding countries ...')
|
||||
|
||||
const countries = await db.country.createMany({ data })
|
||||
|
||||
console.log('Done.', countries)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={9}>
|
||||
<StepHikeCompact.Details title="Seed your database">
|
||||
Run the seed database command to populate the `Country` table with the countries you just created.
|
||||
|
||||
<Admonition type="tip">
|
||||
The reset database command `yarn rw prisma db reset` will recreate the tables and will also run the seed script.
|
||||
</Admonition>
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```bash Terminal
|
||||
yarn rw prisma db seed
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={10}>
|
||||
<StepHikeCompact.Details title="Scaffold the Country UI">
|
||||
Now, we'll use RedwoodJS generators to scaffold a CRUD UI for the `Country` model.
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```bash Terminal
|
||||
yarn rw g scaffold country
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={11}>
|
||||
<StepHikeCompact.Details title="Start the app">
|
||||
Start the app via `yarn rw dev`. A browser will open to the RedwoodJS Splash page.
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||

|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={12}>
|
||||
<StepHikeCompact.Details title="View Countries UI">
|
||||
Click on `/countries` to visit http://localhost:8910/countries where should see the list of countries.
|
||||
|
||||
You may now edit, delete, and add new countries using the scaffolded UI.
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||

|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
|
||||
|
||||
export default Page
|
||||
@@ -36,6 +36,7 @@ Then let's install the additional dependencies: [supabase-js](https://github.com
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
|
||||
npx expo install expo-secure-store
|
||||
```
|
||||
|
||||
Now let's create a helper file to initialize the Supabase client.
|
||||
@@ -44,15 +45,28 @@ These variables will be exposed on the browser, and that's completely fine since
|
||||
[Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
|
||||
```ts title=lib/supabase.ts
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
|
||||
const ExpoSecureStoreAdapter = {
|
||||
getItem: (key: string) => {
|
||||
return SecureStore.getItemAsync(key);
|
||||
},
|
||||
setItem: (key: string, value: string) => {
|
||||
SecureStore.setItemAsync(key, value);
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
SecureStore.deleteItemAsync(key);
|
||||
},
|
||||
};
|
||||
|
||||
const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
|
||||
const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
storage: AsyncStorage as any,
|
||||
storage: ExpoSecureStoreAdapter as any,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
@@ -99,7 +113,7 @@ export default function Auth() {
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Input
|
||||
label="Email"
|
||||
@@ -234,7 +248,7 @@ export default function Account({ session }: { session: Session }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Input label="Email" value={session?.user?.email} disabled />
|
||||
</View>
|
||||
|
||||
@@ -318,6 +318,8 @@ export default function Account({ session }) {
|
||||
|
||||
Let's create a new component for that called `Account.tsx` within a `components` folder.
|
||||
|
||||
First, [generate type definitions from your database using the Supabase CLI](https://supabase.com/docs/guides/database/api/generating-types) to `../utils/database.types`.
|
||||
|
||||
```tsx title=components/Account.tsx
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useUser, useSupabaseClient, Session } from '@supabase/auth-helpers-react'
|
||||
|
||||
@@ -179,11 +179,13 @@ Update your `src/routes/+layout.svelte`:
|
||||
|
||||
export let data: LayoutData
|
||||
|
||||
$: ({ supabase } = data)
|
||||
$: ({ supabase, session } = data)
|
||||
|
||||
onMount(() => {
|
||||
const { data } = supabase.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth')
|
||||
const { data } = supabase.auth.onAuthStateChange((event, _session) => {
|
||||
if (_session?.expires_at !== session?.expires_at) {
|
||||
invalidate('supabase:auth')
|
||||
}
|
||||
})
|
||||
|
||||
return () => data.subscription.unsubscribe()
|
||||
|
||||
@@ -15,7 +15,7 @@ Explore a variety of integrations from Supabase partners. Need a different integ
|
||||
<div>
|
||||
<h2>{item.name}</h2>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 not-prose">
|
||||
{item.items.map((integration) => (
|
||||
{item.items?.map((integration) => (
|
||||
<Link href={`/${integration.url}`} key={integration.title} passHref>
|
||||
<a>
|
||||
<GlassPanel
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'dashibase',
|
||||
title: 'Dashibase',
|
||||
description: 'Create an internal CRUD dashboard with Supabase and Dashibase in 15 mins.',
|
||||
}
|
||||
|
||||
Are you tired of building admin panels? [Dashibase](https://dashibase.com/) is a super fast way to build internal CRUD dashboards with a Notion-like UI.
|
||||
|
||||
This guide walks you through how to build a customer admin panel using Supabase on the backend and Dashibase on the front so that you and your team can easily find and update customer information.
|
||||
|
||||
## Quick explanation
|
||||
|
||||
Just so we are on the same page, here are some of the terms we use:
|
||||
|
||||
- **Data source:** This is the data that powers your dashboard (i.e. your Supabase database).
|
||||
- **Dashboard:** This is the friendly interface that you build on top of your Supabase database using Dashibase.
|
||||
- **Page:** Each dashboard can have multiple pages. Each page can have multiple blocks of text or tables.
|
||||
|
||||
Now that is out of the way, let's build our customer admin panel.
|
||||
|
||||
(Start your stopwatch!)
|
||||
|
||||
## Step 1. Connect Supabase to Dashibase
|
||||
|
||||
First, in Dashibase, go to “Data Sources” via the left sidebar and click on “Connect data source” in the upper-right corner.
|
||||
|
||||
Note: We do not download or store any of your database data.
|
||||
|
||||

|
||||
|
||||
Enter your database credentials, which you can find in Supabase under Settings > Database > Connection info. For additional security, you can also download your SSL certificate and upload it to Dashibase.
|
||||
|
||||

|
||||
|
||||
## Step 2. Set up your data access policy
|
||||
|
||||
Once you have connected your database, you can also control what your team can see and edit in your database via Dashibase. There are usually certain data that you don’t want others to edit or even see (e.g. address, email, identification number).
|
||||
|
||||
Note: If you prefer to restrict access to your database _before_ you connect to Dashibase for security reasons, you can [create a restricted user](https://aws.amazon.com/blogs/database/managing-postgresql-users-and-roles/) via [Supabase's SQL Editor](/docs/guides/database#the-sql-editor). The slight advantage of doing this in Dashibase is that there is a GUI, which makes it easier to set up the access control (but we understand some developers might find it more secure to restrict the access themselves).
|
||||
|
||||

|
||||
|
||||
You can even control which columns within a table that others can see and edit. In the example below, for the `tags` table, the `name` column can be read and updated, the `created_at` column can be read, and the `id` column cannot even be read.
|
||||
|
||||

|
||||
|
||||
Once you are happy with the data access control, you can decide who in your organization can edit this data source (usually other developers) or use this data source to create dashboards.
|
||||
|
||||

|
||||
|
||||
## Step 3. Build your dashboard
|
||||
|
||||
To create a dashboard, go to “Dashboards” via the left sidebar in Dashibase and click on “Create dashboard” in the upper-right corner.
|
||||
|
||||
Here’s where the fun begins. There are many ways to build your dashboards in Dashibase using features such as markdown formatting and slash command.
|
||||
|
||||
You will see some automatically-generated content in your dashboard so that you can set up your dashboard more quickly. For this example, let's clear them out and start from scratch.
|
||||
|
||||

|
||||
|
||||
Using our Notion-like UI, you can quickly build out your dashboard through typing. For example, you can type '/table' to add a table from your database to your dashboard. No more dragging components across your screen and dropping them on a blank canvas repeatedly.
|
||||
|
||||

|
||||
|
||||
If you have set up [foreign keys in your Supabase database](/docs/guides/database/tables#joining-tables-with-foreign-keys), you can easily show data from multiple tables on the table in your Dashibase dashboard. Simply click on the "Columns" button beside the "Filter" button and select the columns you want to show or hide. For this example, we want to show the customers' country and plan price, which are in separate `countries` and `plans` tables.
|
||||
|
||||

|
||||
|
||||
For all the tables in your dashboard, you can rename the column headers, add filter and sort, adjust the column width, and more.
|
||||
|
||||
You can also add text to provide context or instructions for the rest of your team as they are using your dashboard. This saves you the trouble of having your dashboard and documentation in separate places.
|
||||
|
||||

|
||||
|
||||
Most importantly, we automatically generate a single-item view for each of the items on your table so that you do not have to manually create it yourself. You can easily edit or rearrange the blocks just like in Notion.
|
||||
|
||||

|
||||
|
||||
## Step 4. Share your dashboard with your team
|
||||
|
||||
Finally, save and share the dashboard with your team. There are two levels of access:
|
||||
|
||||
- Edit dashboard: Edit the text blocks, edit the tables, etc.
|
||||
- Use dashboard: View and update the data via the dashboard
|
||||
|
||||
Click on the “Share” button in the upper-right corner of your dashboard to share access:
|
||||
|
||||

|
||||
|
||||
Now your team can use this customer admin panel to find and update customer information easily!
|
||||
|
||||
P.S. How long did we take? :)
|
||||
|
||||
## Resources
|
||||
|
||||
- [Dashibase website](https://dashibase.com/)
|
||||
- [Dashibase open-source repo](https://github.com/dashibase)
|
||||
- [Dashibase Twitter](https://twitter.com/dashibase)
|
||||
- [Dashibase Discord](https://discord.gg/crhDFYvbbq)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -200,7 +200,7 @@ If you would like to use Supabase Auth and Prisma in your application, you will
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directURL = env("DIRECT_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
@@ -215,7 +215,7 @@ Next, specify the database schemas you would like to include in your Prisma sche
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directURL = env("DIRECT_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
schemas = ["public", "auth"]
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ If you would like to use a PostgreSQL extension with Prisma, enable the `postgre
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directURL = env("DIRECT_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
@@ -270,7 +270,7 @@ Next, specify the extensions you need in the `datasource` block:
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directURL = env("DIRECT_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
extensions = [hstore(schema: "myHstoreSchema"), pg_trgm, postgis(version: "2.1")]
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,6 @@ export const meta = {
|
||||
video: 'https://www.youtube.com/v/6rcGnW_Mh-0',
|
||||
}
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Custom Domains are currently in beta, and are being slowly made available to projects.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Custom domains allow you to present a branded experience to your users. Custom domains are available as a [add-on for projects on a paid tier](https://app.supabase.com/project/_/settings/billing/update). Setting up a custom domain requires [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. Currently, you must use a subdomain (e.g., `api.example.com`, rather than `example.com`) for the purposes of this guide.
|
||||
|
||||
<div className="video-container">
|
||||
@@ -99,12 +93,6 @@ As with the final activation stage of the process for setting up a custom domain
|
||||
|
||||
## Vanity Subdomains
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Vanity Subdomains are currently in beta, and are being slowly made available to projects. [Contact Support](https://app.supabase.com/support/new) if you'd like to request early access.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Vanity Subdomains allow you to present a basic branded experience, compared to custom domains. They allow you to host your services at a custom subdomain on Supabase (e.g., `my-example-brand.supabase.co`) instead of the default, randomly-assigned `foobarbaz.supabase.co`.
|
||||
|
||||
To get started:
|
||||
|
||||
@@ -34,7 +34,7 @@ After developing your project and deciding it's Production Ready, you should run
|
||||
- Perform load testing (preferably on a staging env)
|
||||
- Tools like [k6](https://k6.io/) can simulate traffic from many different users.
|
||||
- Upgrade your database if you require more resources. If you need anything beyond what is listed, contact enterprise@supabase.io.
|
||||
- If you are expecting a surge in traffic (for a big launch), let the team know by sending your Project Ref to us (support@supabase.io) with more details about your launch. We'll keep an eye on your project.
|
||||
- If you are expecting a surge in traffic (for a big launch) [contact support](https://app.supabase.com/support/new) with more details about your launch. We'll keep an eye on your project.
|
||||
|
||||
## Availability
|
||||
|
||||
@@ -47,14 +47,14 @@ After developing your project and deciding it's Production Ready, you should run
|
||||
- Database backups are not available for download on the free tier.
|
||||
- You can set up your own backup systems using tools like [pg_dump](https://www.postgresqltutorial.com/postgresql-backup-database/) or [wal-g](https://github.com/wal-g/wal-g).
|
||||
- Nightly backups for Pro tier projects are available on the Supabase dashboard for up to 7 days.
|
||||
- Upgrading to the Supabase Pro Tier will give you access to email support on support@supabase.io
|
||||
- Upgrading to the Supabase Pro Tier will give you [access to our support team](https://app.supabase.com/support/new).
|
||||
|
||||
## Rate Limiting, Resource Allocation, & Abuse Prevention
|
||||
|
||||
- Supabase employs a number of safeguards against bursts of incoming traffic to prevent abuse and help maximize stability across the platform
|
||||
- If you're expecting high load events including production launches or heavy load testing, or prolonged high resource usage please give us at least 2 weeks notice. You can do this by opening a ticket via the [support form](https://app.supabase.com/support/new).
|
||||
|
||||
### Rate Limits
|
||||
### Auth Rate Limits
|
||||
|
||||
- The table below shows the rate limit quotas on the following authentication endpoints:
|
||||
|
||||
@@ -69,6 +69,11 @@ After developing your project and deciding it's Production Ready, you should run
|
||||
| Token refresh requests | `/auth/v1/token` | IP Address | 360 requests per hour (with bursts up to 30 requests) |
|
||||
| Create or Verify an MFA challenge | `/auth/v1/factors/:id/challenge` `/auth/v1/factors/:id/verify` | IP Address | 15 requests per minute (with bursts up to 30 requests) |
|
||||
|
||||
### Realtime Rate Limits
|
||||
|
||||
- Review the [Realtime rate limits](/docs/guides/realtime/rate-limits).
|
||||
- If you need rate limits increased you can always [contact support](https://app.supabase.com/support/new).
|
||||
|
||||
### Abuse Prevention
|
||||
|
||||
- Supabase provides CAPTCHA protection on the signup, sign-in and password reset endpoints. Please refer to [our guide](/docs/guides/auth/auth-captcha) on how to protect against abuse using this method.
|
||||
|
||||
@@ -4,23 +4,16 @@ import { logConstants } from 'shared-data'
|
||||
export const meta = {
|
||||
id: 'logs',
|
||||
title: 'Logging',
|
||||
description: 'Getting started with Supabase Platform Log Browser',
|
||||
description: 'Getting started with Supabase Log Browser',
|
||||
}
|
||||
|
||||
The Supabase Platform includes a Logs Explorer that allows log tracing and debugging. Log retention is based on your [project's pricing plan](https://supabase.com/pricing).
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
These features are not currently available for self-hosting and local development.<br/>
|
||||
This is on the roadmap and you can follow the progress in the [Logflare repository](https://github.com/Logflare/logflare).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Product Logs
|
||||
|
||||
Supabase provides a logging interface specific to each product. You can use simple regular expressions for keywords and patterns to search log event messages. You can also export and download the log events matching your query as a spreadsheet.
|
||||
|
||||
{/* <!-- To update the screenshots, ensure that at least one log line is selected to display the metadata. Can use meme.town as an example. --> */}
|
||||
{/* <!-- To update screenshots, ensure that at least one log line is selected to display the metadata. Can use meme.town as an example. --> */}
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
|
||||
@@ -7,7 +7,7 @@ export const meta = {
|
||||
|
||||
Supabase offers a "Spend Cap" on each project to manage your usage and costs.
|
||||
|
||||
This Spend Cap determines whether your project can exceed the free quota allowance of any [line item](#what-can-i-be-charged-for) in a given billing cycle.
|
||||
This Spend Cap determines whether your project can exceed the free quota allowance of any [line item](#what-additional-usage-can-i-be-charged-for) in a given billing cycle.
|
||||
If you exceed the free quota allowance of a line item, and the Spend Cap is **"off"**, then you will be billed for any additional usage on that line item.
|
||||
|
||||
## What additional usage can I be charged for?
|
||||
@@ -35,13 +35,13 @@ From there, you can click on "Change subscription" to select the plan you want t
|
||||
|
||||
## What happens when the Spend Cap is on?
|
||||
|
||||
Once you use all your free quota allowance for a [line item](#what-can-i-be-charged-for), your project will not allow any further usage on that line item until the next billing cycle begins.
|
||||
Once you use all your free quota allowance for a [line item](#what-additional-usage-can-i-be-charged-for), your project will not allow any further usage on that line item until the next billing cycle begins.
|
||||
|
||||
This can help you to avoid unexpected costs and manage your expenses more effectively.
|
||||
|
||||
## What happens when Spend Cap is off?
|
||||
|
||||
Your project will continue to operate after using all the free quota allowance for each [line item](#what-can-i-be-charged-for).
|
||||
Your project will continue to operate after using all the free quota allowance for each [line item](#what-additional-usage-can-i-be-charged-for).
|
||||
This additional usage will be charged based on the line item's cost per unit outlined on the [pricing page](/pricing).
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'bring-your-own-database',
|
||||
title: 'Bring Your Own Database',
|
||||
description: 'Bring your own Postgres database to subscribe to Realtime changes',
|
||||
sidebar_label: 'Bring Your Own Database',
|
||||
}
|
||||
|
||||
Realtime Database Changes works with any Postgres database that has logical replication enabled and the [wal2json](https://github.com/eulerto/wal2json) extension installed.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
After setting up your database for Realtime, [submit a support ticket](https://app.supabase.com/support/new) if you want to use your database with hosted Supabase Realtime.
|
||||
|
||||
</Admonition>
|
||||
|
||||
The following steps will make sure that your database is properly set up to work with Realtime.
|
||||
|
||||
## `wal2json` Extension
|
||||
|
||||
Realtime relies on the `wal2json` Postgres extension to format database changes to JSON which are then sent to Realtime subscribers.
|
||||
|
||||
Postgres databases managed by AWS RDS and Google Cloud SQL should already have `wal2json` installed.
|
||||
Please check to make sure this is indeed the case with your Postgres database.
|
||||
|
||||
## Logical Replication Configuration
|
||||
|
||||
Realtime relies on Postgres' logical replication functionality to get database changes. Please enable logical replication on your database and configure the following settings:
|
||||
|
||||
- `max_replication_slots`: we recommend `10` because Realtime requires a few slots plus the slots you'll need for your non-Realtime logical replication needs.
|
||||
- `max_slot_wal_keep_size`: we recommend `1024` (MB) so Realtime can attempt to deliver more database changes stored in Postgres.
|
||||
|
||||
## Realtime Database Setup
|
||||
|
||||
### `supabase_realtime` Publication
|
||||
|
||||
Create `supabase_realtime` publication and add tables you want Realtime to listen to:
|
||||
|
||||
```sql
|
||||
create publication supabase_realtime with (publish = 'insert, update, delete');
|
||||
|
||||
alter publication supabase_realtime add table messages, users;
|
||||
```
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
If the payload contains a `401 Unauthorized` then you will need to grant `select` privileges to the table for the database role you want to authorize to receive database changes:
|
||||
|
||||
```sql
|
||||
grant select on table messages to anon;
|
||||
```
|
||||
|
||||
</Admonition>
|
||||
|
||||
### `realtime` Schema
|
||||
|
||||
Create a `realtime` schema:
|
||||
|
||||
```sql
|
||||
create schema realtime;
|
||||
```
|
||||
|
||||
### `supabase_realtime_admin` Role
|
||||
|
||||
Create a `supabase_realtime_admin` database role and grant it replication permissions:
|
||||
|
||||
```sql
|
||||
create role supabase_realtime_admin with noinherit login password 'secure-password';
|
||||
```
|
||||
|
||||
Make sure to grant `supabase_realtime_admin` role with replication permissions. This step will vary based on your database provider.
|
||||
|
||||
For example, if your database is managed by AWS RDS then you can run:
|
||||
|
||||
```sql
|
||||
grant rds_replication to supabase_realtime_admin;
|
||||
```
|
||||
|
||||
### `supabase_realtime_admin` Privileges
|
||||
|
||||
Grant `supabase_realtime_admin` privileges for `realtime` schema and all related Realtime objects:
|
||||
|
||||
```sql
|
||||
grant all on schema realtime to supabase_realtime_admin;
|
||||
grant all on all tables in schema realtime to supabase_realtime_admin;
|
||||
grant all on all sequences in schema realtime to supabase_realtime_admin;
|
||||
grant all on all routines in schema realtime to supabase_realtime_admin;
|
||||
```
|
||||
|
||||
### `authenticated` Role
|
||||
|
||||
Create an `authenticated` role:
|
||||
|
||||
```sql
|
||||
create role authenticated nologin noinherit;
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
This role is hardcoded into Realtime migrations so it's required for the time being.
|
||||
|
||||
</Admonition>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
Loaded 100 of 1165 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user