mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
Merge commit '803c92a0cce185a5434d63a7c5ddb70bc45312e3' of https://github.com/gitstart/supabase into fix/table-editor-deleting-pagination-bug
This commit is contained in:
commit
b89950a880
729 files changed
+28624
-89429
No files matched your search
@@ -1,32 +0,0 @@
|
||||
name: Studio Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- studio
|
||||
paths:
|
||||
- 'studio/**'
|
||||
- 'package-lock.json'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
working-directory: ./
|
||||
- name: Run build
|
||||
run: npx turbo run build --filter=studio
|
||||
working-directory: ./
|
||||
@@ -136,3 +136,9 @@ Create a new entry in the [`redirects.js`](https://github.com/supabase/supabase/
|
||||
## Community channels
|
||||
|
||||
Stuck somewhere? Have any questions? Join the [Discord Community Server](https://discord.supabase.com/) or the [Github Discussions](https://github.com/supabase/supabase/discussions). We are here to help!
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/supabase/supabase/graphs/contributors">
|
||||
<img src="https://contributors.deno.dev/supabase/supabase?height=1200&width=1200&count=90" width="1200" height="1200" alt="contributors">
|
||||
</a>
|
||||
+1
-1
@@ -1 +1 @@
|
||||
apps/reference/static/.well-known/security.txt
|
||||
apps/docs/public/.well-known/security.txt
|
||||
@@ -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()
|
||||
|
||||
@@ -5,26 +5,26 @@ from `public.profiles` and the older versions getting orphaned.
|
||||
To automatically remove obsolete storage objects, extend the database
|
||||
triggers. Note that it is not sufficient to delete the objects from the
|
||||
`storage.objects` table because that would orphan and leak the actual storage objects in
|
||||
the S3 backend. Instead, invoke the storage API within Postgres via the `http` extension.
|
||||
the S3 backend. Instead, invoke the storage API within Postgres via the `http` extension.
|
||||
|
||||
Enable the [http extension for the `extensions` schema](https://app.supabase.com/project/_/database/extensions) in the Dashboard.
|
||||
Then, define the following SQL functions in the SQL Editor to delete
|
||||
storage objects via the API:
|
||||
|
||||
```SQL
|
||||
create or replace function delete_storage_object(bucket text, object text, out status int, out content varchar)
|
||||
create or replace function delete_storage_object(bucket text, object text, out status int, out content text)
|
||||
returns record
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
project_url varchar := '<YOURPROJECTURL>';
|
||||
service_role_key varchar := '<YOURSERVICEROLEKEY>'; -- full access needed
|
||||
url varchar := project_url||'/storage/v1/object/'||bucket||'/'||object;
|
||||
project_url text := '<YOURPROJECTURL>';
|
||||
service_role_key text := '<YOURSERVICEROLEKEY>'; -- full access needed
|
||||
url text := project_url||'/storage/v1/object/'||bucket||'/'||object;
|
||||
begin
|
||||
select
|
||||
into status, content
|
||||
result.status::int, result.content::varchar
|
||||
result.status::int, result.content::text
|
||||
FROM extensions.http((
|
||||
'DELETE',
|
||||
url,
|
||||
@@ -34,7 +34,7 @@ begin
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function delete_avatar(avatar_url text, out status int, out content varchar)
|
||||
create or replace function delete_avatar(avatar_url text, out status int, out content text)
|
||||
returns record
|
||||
language 'plpgsql'
|
||||
security definer
|
||||
@@ -60,7 +60,7 @@ security definer
|
||||
as $$
|
||||
declare
|
||||
status int;
|
||||
content varchar;
|
||||
content text;
|
||||
begin
|
||||
if coalesce(old.avatar_url, '') <> ''
|
||||
and (tg_op = 'DELETE' or (old.avatar_url <> new.avatar_url)) then
|
||||
|
||||
@@ -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,13 +1,12 @@
|
||||
import { useTheme } from 'common/Providers'
|
||||
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 { FC, useEffect, useState } from 'react'
|
||||
import { IconCommand, IconMenu, IconMoon, IconSearch, IconSun, Listbox, SearchButton } from 'ui'
|
||||
import { REFERENCES } from './Navigation.constants'
|
||||
|
||||
import { getPageType } from '~/lib/helpers'
|
||||
import SearchButton from '../Search/SearchButton'
|
||||
|
||||
const NavBar: FC = () => {
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
@@ -36,14 +35,6 @@ const NavBar: 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' : ''
|
||||
}
|
||||
|
||||
const onSelectVersion = (version: string) => {
|
||||
// [Joshen] Ideally we use <Link> but this works for now
|
||||
if (!library) return
|
||||
@@ -183,7 +174,7 @@ const NavBar: FC = () => {
|
||||
</a>
|
||||
</li>
|
||||
<li className="px-4">
|
||||
<div className="cursor-pointer" onClick={toggleDarkMode}>
|
||||
<div className="cursor-pointer" onClick={() => toggleTheme()}>
|
||||
{isDarkMode ? (
|
||||
<IconMoon size={18} strokeWidth={2} className="text-scale-1200" />
|
||||
) : (
|
||||
|
||||
@@ -241,12 +241,62 @@ export const menuItems: NavMenu = {
|
||||
{
|
||||
label: 'Realtime',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/realtime', items: [] },
|
||||
{ name: 'Quickstart', url: '/guides/realtime/quickstart', 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: 'Rate Limits', url: '/guides/realtime/rate-limits', 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: 'Bring Your Own Database',
|
||||
url: '/guides/realtime/bring-your-own-database',
|
||||
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: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ const home = [
|
||||
{
|
||||
label: 'Auth',
|
||||
icon: '/img/icons/menu/auth',
|
||||
href: '/guides/auth/overview',
|
||||
href: '/guides/auth',
|
||||
level: 'auth',
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -205,7 +205,11 @@ const NavigationMenu = () => {
|
||||
<NavigationMenuGuideList id={'database'} active={isDatabaseActive} />
|
||||
<NavigationMenuGuideList id={'auth'} active={isAuthActive} />
|
||||
<NavigationMenuGuideList id={'functions'} active={isFunctionsActive} />
|
||||
<NavigationMenuGuideList id={'realtime'} active={isRealtimeActive} />
|
||||
<NavigationMenuGuideList
|
||||
id={'realtime'}
|
||||
active={isRealtimeActive}
|
||||
value={['/guides/realtime/extensions']}
|
||||
/>
|
||||
<NavigationMenuGuideList id={'storage'} active={isStorageActive} />
|
||||
<NavigationMenuGuideList id={'supabase_cli'} active={issupabase_cliActive} />
|
||||
<NavigationMenuGuideList id={'platform'} active={isPlatformActive} />
|
||||
|
||||
@@ -2,14 +2,19 @@ import * as Accordion from '@radix-ui/react-accordion'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import NavigationMenuGuideListItems from './NavigationMenuGuideListItems'
|
||||
import * as NavItems from './NavigationMenu.constants'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
active: boolean
|
||||
collapsible?: boolean
|
||||
value?: string[]
|
||||
}
|
||||
const NavigationMenuGuideList: React.FC<Props> = ({ id, active }) => {
|
||||
const NavigationMenuGuideList: React.FC<Props> = ({ id, active, value }) => {
|
||||
const router = useRouter()
|
||||
|
||||
const menu = NavItems[id]
|
||||
|
||||
// get url
|
||||
const url = router.asPath
|
||||
|
||||
@@ -26,10 +31,10 @@ const NavigationMenuGuideList: React.FC<Props> = ({ id, active }) => {
|
||||
|
||||
return (
|
||||
<Accordion.Root
|
||||
collapsible
|
||||
collapsible={true}
|
||||
key={id}
|
||||
type="single"
|
||||
value={firstLevelRoute}
|
||||
type={value ? 'multiple' : 'single'}
|
||||
value={value ?? firstLevelRoute}
|
||||
className={[
|
||||
'transition-all ml-8 duration-150 ease-out',
|
||||
// enabled
|
||||
@@ -41,7 +46,7 @@ const NavigationMenuGuideList: React.FC<Props> = ({ id, active }) => {
|
||||
!active ? 'opacity-0 invisible absolute h-0 overflow-hidden' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
<NavigationMenuGuideListItems id={id} />
|
||||
<NavigationMenuGuideListItems menu={menu} id={id} />
|
||||
</Accordion.Root>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -143,9 +143,7 @@ const ContentLink = React.memo(function ContentLink(props: any) {
|
||||
})
|
||||
|
||||
const Content = (props) => {
|
||||
const { id } = props
|
||||
|
||||
const menu = NavItems[id]
|
||||
const { menu, id } = props
|
||||
|
||||
return (
|
||||
<ul className={['relative w-full flex flex-col gap-0'].join(' ')}>
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
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 {
|
||||
Button,
|
||||
IconCommand,
|
||||
IconMenu,
|
||||
IconMoon,
|
||||
IconSearch,
|
||||
IconSun,
|
||||
Listbox,
|
||||
SearchButton,
|
||||
} 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'
|
||||
|
||||
const TopNavBar: FC = () => {
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
@@ -36,14 +44,6 @@ const TopNavBar: 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' : ''
|
||||
}
|
||||
|
||||
const onSelectVersion = (version: string) => {
|
||||
// [Joshen] Ideally we use <Link> but this works for now
|
||||
if (!library) return
|
||||
@@ -147,7 +147,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,10 +3,9 @@ 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 { Button, IconCommand, IconGitHub, IconMoon, IconSearch, IconSun, SearchButton } from 'ui'
|
||||
import { REFERENCES } from '~/components/Navigation/Navigation.constants'
|
||||
|
||||
import SearchButton from '~/components/Search/SearchButton'
|
||||
import { getPageType } from '~/lib/helpers'
|
||||
|
||||
const TopNavBarRef: FC = () => {
|
||||
@@ -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
|
||||
@@ -149,7 +139,7 @@ const TopNavBarRef: FC = () => {
|
||||
</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}
|
||||
|
||||
@@ -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,30 +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'
|
||||
|
||||
@@ -33,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,
|
||||
@@ -50,6 +47,7 @@ const components = {
|
||||
ProjectSetup,
|
||||
SocialProviderSetup,
|
||||
SocialProviderSettingsSupabase,
|
||||
StepHikeCompact,
|
||||
StorageManagement,
|
||||
Mermaid,
|
||||
Extensions,
|
||||
@@ -70,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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
@@ -30,51 +31,78 @@ const RefSectionHandler = (props: RefSectionHandlerProps) => {
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<RefSubLayout>
|
||||
{props.sections.map((x, i) => {
|
||||
switch (x.type) {
|
||||
case 'markdown':
|
||||
const markdownData = props.pageProps.docs.find((doc) => doc.id === x.id)
|
||||
function getPageTitle() {
|
||||
switch (props.type) {
|
||||
case 'client-lib':
|
||||
return props.spec.info.title
|
||||
case 'cli':
|
||||
return 'Supabase CLI reference'
|
||||
case 'api':
|
||||
return 'Supabase API reference'
|
||||
default:
|
||||
return 'Supabase Docs'
|
||||
}
|
||||
}
|
||||
|
||||
return <RefEducationSection key={x.id + i} item={x} markdownContent={markdownData} />
|
||||
break
|
||||
case 'function':
|
||||
return (
|
||||
<RefFunctionSection
|
||||
key={x.id + i}
|
||||
funcData={x}
|
||||
commonFuncData={x}
|
||||
spec={props.spec}
|
||||
typeSpec={props.typeSpec}
|
||||
/>
|
||||
)
|
||||
case 'cli-command':
|
||||
return <CliCommandSection key={x.id + i} funcData={x} commonFuncData={x} />
|
||||
break
|
||||
case 'operation':
|
||||
return (
|
||||
<ApiOperationSection
|
||||
key={x.id + i}
|
||||
funcData={x}
|
||||
commonFuncData={x}
|
||||
spec={props.spec}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<RefFunctionSection
|
||||
key={x.id + i}
|
||||
funcData={x}
|
||||
commonFuncData={x}
|
||||
spec={props.spec}
|
||||
typeSpec={props.typeSpec}
|
||||
/>
|
||||
)
|
||||
break
|
||||
}
|
||||
})}
|
||||
</RefSubLayout>
|
||||
const pageTitle = getPageTitle()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={pageTitle} />
|
||||
<meta property="og:image" content={`https://supabase.com/docs/img/supabase-og-image.png`} />
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content={`https://supabase.com/docs/img/supabase-og-image.png`}
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</Head>
|
||||
<RefSubLayout>
|
||||
{props.sections.map((x, i) => {
|
||||
switch (x.type) {
|
||||
case 'markdown':
|
||||
const markdownData = props.pageProps.docs.find((doc) => doc.id === x.id)
|
||||
|
||||
return <RefEducationSection key={x.id + i} item={x} markdownContent={markdownData} />
|
||||
break
|
||||
case 'function':
|
||||
return (
|
||||
<RefFunctionSection
|
||||
key={x.id + i}
|
||||
funcData={x}
|
||||
commonFuncData={x}
|
||||
spec={props.spec}
|
||||
typeSpec={props.typeSpec}
|
||||
/>
|
||||
)
|
||||
case 'cli-command':
|
||||
return <CliCommandSection key={x.id + i} funcData={x} commonFuncData={x} />
|
||||
break
|
||||
case 'operation':
|
||||
return (
|
||||
<ApiOperationSection
|
||||
key={x.id + i}
|
||||
funcData={x}
|
||||
commonFuncData={x}
|
||||
spec={props.spec}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<RefFunctionSection
|
||||
key={x.id + i}
|
||||
funcData={x}
|
||||
commonFuncData={x}
|
||||
spec={props.spec}
|
||||
typeSpec={props.typeSpec}
|
||||
/>
|
||||
)
|
||||
break
|
||||
}
|
||||
})}
|
||||
</RefSubLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import TopNavBarRef from '~/components/Navigation/NavigationMenu/TopNavBarRef'
|
||||
import { memo, useEffect } from 'react'
|
||||
import Footer from '~/components/Navigation/Footer'
|
||||
import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState'
|
||||
import Head from 'next/head'
|
||||
|
||||
const levelsData = {
|
||||
home: {
|
||||
@@ -312,32 +313,37 @@ const SiteLayout = ({ children }) => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="flex flex-row h-screen">
|
||||
<NavContainer />
|
||||
<Container>
|
||||
<div className={['lg:sticky top-0 z-10 overflow-hidden'].join(' ')}>
|
||||
<TopNavBarRef />
|
||||
</div>
|
||||
<div
|
||||
className={[
|
||||
'sticky transition-all top-0',
|
||||
'z-10',
|
||||
'backdrop-blur backdrop-filter bg-white-1200 dark:bg-blackA-300',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className={['lg:hidden', 'px-5 ', 'border-b z-10'].join(' ')}>
|
||||
<MobileHeader />
|
||||
<>
|
||||
<Head>
|
||||
<title>Supabase Docs</title>
|
||||
</Head>
|
||||
<main>
|
||||
<div className="flex flex-row h-screen">
|
||||
<NavContainer />
|
||||
<Container>
|
||||
<div className={['lg:sticky top-0 z-10 overflow-hidden'].join(' ')}>
|
||||
<TopNavBarRef />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow px-5 max-w-7xl mx-auto py-16">
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
<MobileMenuBackdrop />
|
||||
</Container>
|
||||
</div>
|
||||
</main>
|
||||
<div
|
||||
className={[
|
||||
'sticky transition-all top-0',
|
||||
'z-10',
|
||||
'backdrop-blur backdrop-filter bg-white-1200 dark:bg-blackA-300',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className={['lg:hidden', 'px-5 ', 'border-b z-10'].join(' ')}>
|
||||
<MobileHeader />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow px-5 max-w-7xl mx-auto py-16">
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
<MobileMenuBackdrop />
|
||||
</Container>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ const Layout: FC<Props> = (props) => {
|
||||
// page type, ie, Auth, Database, Storage etc
|
||||
const ogPageType = asPath.split('/')[2]
|
||||
// open graph image url constructor
|
||||
const ogImageUrl = encodeURIComponent(
|
||||
const ogImageUrl = encodeURI(
|
||||
`https://obuldanrptloktxcffvn.functions.supabase.co/og-images?site=docs${
|
||||
ogPageType ? `&type=${ogPageType}` : ''
|
||||
}&title=${props.meta?.title}&description=${props.meta?.description}`
|
||||
|
||||
@@ -37,7 +37,6 @@ const RefSubLayoutNonFunc: FC<RefSubLayoutNonFuncType> & RefSubLayoutNonFuncSubC
|
||||
}
|
||||
|
||||
const Section: FC<ISectionContainer> = (props) => {
|
||||
// console.log({ props })
|
||||
return (
|
||||
<article
|
||||
key={props.id}
|
||||
|
||||
@@ -80,6 +80,16 @@ const nextConfig = {
|
||||
},
|
||||
]
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/docs',
|
||||
basePath: false,
|
||||
permanent: false,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
// next.config.js
|
||||
|
||||
@@ -45,9 +45,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",
|
||||
|
||||
+24
-20
@@ -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'
|
||||
@@ -62,28 +61,33 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
File renamed without changes.
@@ -158,7 +158,7 @@ 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()
|
||||
|
||||
@@ -185,7 +185,8 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to use the `supabaseClient` from the `useSupabaseClient` hook and only run your query once the user is defined client-side in the `useUser()` hook:
|
||||
|
||||
```jsx lines=10-17
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
import { ThemeSupa } from '@supabase/auth-ui-shared'
|
||||
import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
|
||||
@@ -91,13 +91,12 @@ export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
|
||||
};
|
||||
```
|
||||
|
||||
#### Shared Load functions and pages
|
||||
### Shared Load functions and pages
|
||||
|
||||
To be able to use Supabase in shared load functions and inside pages you need to create a Supabase client in the root layout load:
|
||||
|
||||
```ts
|
||||
// src/routes/+layout.ts
|
||||
import { invalidate } from '$app/navigation';
|
||||
import {
|
||||
PUBLIC_SUPABASE_ANON_KEY,
|
||||
PUBLIC_SUPABASE_URL
|
||||
@@ -113,10 +112,7 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
supabaseUrl: PUBLIC_SUPABASE_URL,
|
||||
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
|
||||
event: { fetch },
|
||||
serverSession: data.session,
|
||||
onAuthStateChange() {
|
||||
invalidate('supabase:auth');
|
||||
}
|
||||
serverSession: data.session
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -129,10 +125,41 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
|
||||
Access the client inside pages by `$page.data.supabase` or `data.supabase` when using `export let data: PageData`.
|
||||
|
||||
The usage of `depends` and `invalidate` tells sveltekit that this load function should be executed whenever the session updates to keep the page store in sync.
|
||||
The usage of `depends` tells sveltekit that this load function should be executed whenever `invalidate` is called to keep the page store in sync.
|
||||
|
||||
`createSupabaseLoadClient` caches the client when running in a browser environment and therefore does not create a new client for every time the load function runs.
|
||||
|
||||
### Setting up the event listener on the client side
|
||||
|
||||
We need to create an event listener in the root `+layout.svelte` file in order catch supabase events being triggered.
|
||||
|
||||
```svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
$: ({ supabase } = data);
|
||||
|
||||
onMount(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth');
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
```
|
||||
|
||||
The usage of `invalidate` tells sveltekit that the root `+layout.ts` load function should be executed whenever the session updates to keep the page store in sync.
|
||||
|
||||
### Generate types from your database
|
||||
|
||||
In order to get the most out of TypeScript and it's intellisense, you should import the generated Database types into the `app.d.ts` type definition file that comes with your SvelteKit project, where `import('./DatabaseDefinitions')` points to the generated types file outlined in [v2 docs here](https://supabase.com/docs/reference/javascript/release-notes#typescript-support) after you have logged in, linked, and generated types through the Supabase CLI.
|
||||
@@ -183,7 +210,7 @@ You can now determine if a user is authenticated on the client-side by checking
|
||||
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:
|
||||
|
||||
```html
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
@@ -272,7 +299,7 @@ Wrap an Action to check that the user has a valid session. If they're not logged
|
||||
```ts
|
||||
// src/routes/posts/+page.server.ts
|
||||
import type { Actions } from './$types';
|
||||
import { error, invalid } from '@sveltejs/kit';
|
||||
import { error, fail } from '@sveltejs/kit';
|
||||
|
||||
export const actions: Actions = {
|
||||
createPost: async ({ request, locals: { supabase, getSession } }) => {
|
||||
@@ -291,7 +318,7 @@ export const actions: Actions = {
|
||||
.insert({ content });
|
||||
|
||||
if (createPostError) {
|
||||
return invalid(500, {
|
||||
return fail(500, {
|
||||
supabaseErrorMessage: createPostError.message
|
||||
});
|
||||
}
|
||||
@@ -308,7 +335,7 @@ If you try to submit a form with the action `?/createPost` without a valid sessi
|
||||
|
||||
```ts
|
||||
import type { Actions } from './$types';
|
||||
import { invalid, redirect } from '@sveltejs/kit';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { AuthApiError } from '@supabase/supabase-js';
|
||||
|
||||
export const actions: Actions = {
|
||||
@@ -325,14 +352,14 @@ export const actions: Actions = {
|
||||
|
||||
if (error) {
|
||||
if (error instanceof AuthApiError && error.status === 400) {
|
||||
return invalid(400, {
|
||||
return fail(400, {
|
||||
error: 'Invalid credentials.',
|
||||
values: {
|
||||
email
|
||||
}
|
||||
});
|
||||
}
|
||||
return invalid(500, {
|
||||
return fail(500, {
|
||||
error: 'Server error. Try again later.',
|
||||
values: {
|
||||
email
|
||||
@@ -461,7 +488,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
|
||||
### Initialize the client [#migration-initialize-client]
|
||||
|
||||
In order to use the Supabase library in your client code you will need to setup a shared load function inside the root `+layout.ts` file.
|
||||
In order to use the Supabase library in your client code you will need to setup a shared load function inside the root `+layout.ts` and create a `+layout.svelte` to handle our event listening for Auth events.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -473,7 +500,7 @@ In order to use the Supabase library in your client code you will need to setup
|
||||
|
||||
```html title=src/routes/+layout.svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { supabaseClient } from '$lib/db'
|
||||
import { invalidate } from '$app/navigation'
|
||||
import { onMount } from 'svelte'
|
||||
@@ -515,10 +542,7 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
supabaseUrl: PUBLIC_SUPABASE_URL,
|
||||
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
|
||||
event: { fetch },
|
||||
serverSession: data.session,
|
||||
onAuthStateChange() {
|
||||
invalidate('supabase:auth');
|
||||
}
|
||||
serverSession: data.session
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -529,6 +553,31 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
};
|
||||
```
|
||||
|
||||
```svelte title=src/routes/+layout.svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
$: ({ supabase } = data);
|
||||
|
||||
onMount(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth');
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -606,7 +655,7 @@ declare global {
|
||||
|
||||
```html title=src/routes/profile/+page.svelte
|
||||
<!-- src/routes/profile/+page.svelte -->
|
||||
<script>
|
||||
<script lang="ts">
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data
|
||||
$: ({ user, tableData } = data)
|
||||
|
||||
@@ -328,6 +328,9 @@ Tip: Make sure to enable RLS for all your tables, so that your tables are inacce
|
||||
|
||||
Supabase provides special "Service" keys, which can be used to bypass all RLS.
|
||||
These should never be used in the browser or exposed to customers, but they are useful for administrative tasks.
|
||||
<Admonition type="caution">
|
||||
Initializing a client with a Service Key will not override RLS if a user token is set. If a user is signed in with a client, Supabase will adhere to the RLS policy of the user.
|
||||
</Admonition>
|
||||
|
||||
### Testing policies
|
||||
|
||||
|
||||
@@ -36,8 +36,9 @@ Go to the [GitHub Developer Settings](https://github.com/settings/developers) pa
|
||||
|
||||
<SocialProviderSetup provider="GitHub" />
|
||||
|
||||
## Register a new OAuth application
|
||||
## Register a new OAuth application on Github.
|
||||
|
||||
- Navigate to `Settings`/`Developer settings`/`OAuth Apps`
|
||||
- Click `Register a new application`. If you've created an app before, click `New OAuth App` here.
|
||||
- In `Application name`, type the name of your app.
|
||||
- In `Homepage URL`, type the full URL to your app's website.
|
||||
|
||||
@@ -160,7 +160,7 @@ curl -X POST \
|
||||
-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>'
|
||||
'https://<project>.supabase.co/auth/v1/admin/sso/providers'
|
||||
```
|
||||
|
||||
To create the `/tmp/body.json` file you can use this:
|
||||
|
||||
@@ -7,7 +7,23 @@ export const meta = {
|
||||
video: 'https://www.youtube.com/v/vyHyYpvjaks',
|
||||
}
|
||||
|
||||
Learn how to use the Supabase CLI to develop your project locally and deploy to the Supabase Platform.
|
||||
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 applying those migrations to a linked project on the [Platform](https://app.supabase.io/) keeps everything nicely organized as you grow.
|
||||
|
||||
## Why develop locally?
|
||||
|
||||
The Dashboard provides a wide range of features for setting up your project: creating tables, adding columns, changing existing columns, creating views, setting up RLS policies, and more. Given all of the Dashboard's capabilities, you might question the need to work locally. Here's a few advantages to working this way:
|
||||
|
||||
1. **Faster Development**: Developing locally allows you to work without any network latency or internet disruptions.
|
||||
|
||||
2. **Easier Collaboration**: Developing locally can make it easier to collaborate with others on the same project.
|
||||
|
||||
3. **Cost-Effective**: Supabase provides a generous free tier and gives you two free projects to get started. But what if you need more than two? When you develop locally, you can spin up unlimited local projects and link them with live projects when you're ready to launch.
|
||||
|
||||
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 plane? An automobile? No problem. Developing your project locally allows you to work offline.
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
@@ -49,7 +65,7 @@ mkdir your-project
|
||||
# move into the new folder
|
||||
cd your-project
|
||||
|
||||
# start a new git repository
|
||||
# start a new git repository — important, don't skip this step
|
||||
git init
|
||||
```
|
||||
|
||||
@@ -68,12 +84,26 @@ This command may take a while to run if this is the first time using the CLI.
|
||||
supabase start
|
||||
```
|
||||
|
||||
Once all of the Supabase services are running, you'll see output containing your local Supabase credentials.
|
||||
You can use the [stop](/docs/reference/cli/usage#supabase-stop) command at any time to stop all services.
|
||||
Once all of the Supabase services are running, you'll see output containing your local Supabase credentials. It should look like this, with urls and keys that you'll use in your local project:
|
||||
|
||||
## Access services
|
||||
```
|
||||
|
||||
You can access services directly with any Postgres client or through the API Gateway ([Kong](https://github.com/Kong/kong)).
|
||||
Started supabase local development setup.
|
||||
|
||||
API URL: http://localhost:54321
|
||||
DB URL: postgresql://postgres:postgres@localhost:54322/postgres
|
||||
Studio URL: http://localhost:54323
|
||||
Inbucket URL: http://localhost:54324
|
||||
anon key: eyJh......
|
||||
service_role key: eyJh......
|
||||
|
||||
```
|
||||
|
||||
You can use the [supabase stop](/docs/reference/cli/usage#supabase-stop) command at any time to stop all services.
|
||||
|
||||
## Access your project's services
|
||||
|
||||
You can now visit your local Dashboard at [http://localhost:54323](http://localhost:54323), and access the database directly with any Postgres client via `postgresql://postgres:postgres@localhost:54322/postgres`.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -147,95 +177,218 @@ Database changes are managed through "migrations." Database migrations are a com
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
### Make database changes
|
||||
For this guide, we'll create a table called `employees` and see how we can make changes to it.
|
||||
|
||||
For this guide, create a table called `employees`. In Supabase Studio, navigate to the **SQL Editor** page and run the following SQL command:
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details title="Create your first migration file">
|
||||
|
||||
To get started, generate a [new migration](https://supabase.com/docs/reference/cli/supabase-migration-new) to store the SQL needed to create our `employees` table
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash
|
||||
supabase migration new create_employees_table
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
<StepHikeCompact.Details title="Add the SQL to your migration file">
|
||||
This creates a new migration: supabase/migrations/\<timestamp\>
|
||||
_create_employees_table.sql.
|
||||
|
||||
To that file, add the SQL to create this `employees` table
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```sql
|
||||
|
||||
```sql
|
||||
create table
|
||||
employees (
|
||||
id integer primary key generated always as identity,
|
||||
name text
|
||||
);
|
||||
```
|
||||
employees (
|
||||
id bigint primary key generated always as identity,
|
||||
name text,
|
||||
email text,
|
||||
created_at timestamptz default now()
|
||||
);
|
||||
|
||||
<Admonition type="note">
|
||||
````
|
||||
|
||||
You can execute any SQL using the `DB URL` shown by [`supabase status`](/docs/reference/cli/usage#supabase-status).
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</Admonition>
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
Run the [`db diff`](/docs/reference/cli/usage#supabase-db-diff) command to detect changes in the local database:
|
||||
|
||||
```sh
|
||||
supabase db diff create_employees -f create_employees
|
||||
```
|
||||
|
||||
This creates a new migration named `supabase/migrations/<timestamp>_create_employees.sql`, representing any changes made to the local database since [`supabase start`](/docs/reference/cli/usage#supabase-start).
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Apply your migration">
|
||||
Now that you have a migration file, you can run this migration and create the `employees` table.
|
||||
|
||||
Use the `reset` command here to reset the database to the current migrations
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```bash
|
||||
supabase db reset
|
||||
````
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Modify your employees table">
|
||||
Now you can visit your new `employees` table in the Dashboard.
|
||||
|
||||
Next, modify your `employees` table by adding a column for department. Create a new migration file for that.
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```bash
|
||||
supabase migration new add_department_to_employees_table
|
||||
|
||||
````
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
<StepHikeCompact.Details title="Add a new column to your table">
|
||||
This creates a new migration file: supabase/migrations/\<timestamp\>
|
||||
_add_department_to_employees_table.sql.
|
||||
|
||||
To that file, add the SQL to create a new department column
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```sql
|
||||
alter table
|
||||
if exists public.employees add department text default 'Hooli';
|
||||
````
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
### Add sample data
|
||||
|
||||
Use the seed script in `supabase/seed.sql` (created with [`supabase init`](/docs/reference/cli/usage#supabase-init)) to add sample data to the table.
|
||||
Now that you are managing your database with migrations scripts, it would be great have some seed data to use every time you reset the database.
|
||||
|
||||
For this, you can use the seed script in `supabase/seed.sql`. This file was automatically created when you ran [`supabase init`](/docs/reference/cli/usage#supabase-init)) at the beginning.
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details title="Populate your table">
|
||||
Insert data into your `employees` table with your `supabase/seed.sql` file.
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```sql
|
||||
|
||||
```sql
|
||||
-- in supabase/seed.sql
|
||||
insert into public.employees
|
||||
(name)
|
||||
insert into
|
||||
public.employees (name)
|
||||
values
|
||||
('Erlich Bachman'),
|
||||
('Richard Hendricks'),
|
||||
('Monica Hall');
|
||||
```
|
||||
('Erlich Bachman'),
|
||||
('Richard Hendricks'),
|
||||
('Monica Hall');
|
||||
|
||||
Rerun the migration and seed scripts:
|
||||
````
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
```bash
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
<StepHikeCompact.Details title="Reset your database">
|
||||
Reset your database (apply current migrations), and populate with seed data
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```bash
|
||||
supabase db reset
|
||||
````
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
You should now see the `employees` table, along with your seed data in the Dashboard! All of your database changes are captured in code, and you can reset to a known state at any time, complete with seed data.
|
||||
|
||||
### Diffing changes
|
||||
|
||||
This workflow is great if you know SQL and are comfortable creating tables and columns. If not, you can still use the Dashboard to create tables and columns, and then use the CLI to diff your changes and create migrations.
|
||||
|
||||
Create a new table called `cities`, with columns `id`, `name` and `population`. To see the corresponding SQL for this, you can use the `supabase db diff --schema public` command. This will show you the SQL that will be run to create the table and columns. The output of `supabase db diff` will look something like this:
|
||||
|
||||
```
|
||||
Diffing schemas: public
|
||||
Finished supabase db diff on branch main.
|
||||
|
||||
create table "public"."cities" (
|
||||
"id" bigint primary key generated always as identity,
|
||||
"name" text,
|
||||
"population" bigint
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
You should now see the contents of `employees` in Studio.
|
||||
You can then copy this SQL into a new migration file, and run `supabase db reset` to apply the changes.
|
||||
|
||||
### Reset database changes
|
||||
|
||||
Use the [`reset`](/docs/reference/cli/usage#supabase-db-reset) command to revert any changes to the local database.
|
||||
|
||||
```sql
|
||||
-- run on local database to make a change
|
||||
alter table employees
|
||||
add department text default 'Hooli';
|
||||
```
|
||||
|
||||
Run the following command to reset the local database:
|
||||
|
||||
```sh
|
||||
supabase db reset
|
||||
```
|
||||
The last step is deploying these changes to a live Supabase project.
|
||||
|
||||
## Deploy your project
|
||||
|
||||
Go to the [Supabase Dashboard](https://app.supabase.com) and create a project to deploy the changes.
|
||||
You've been developing your project locally, making changes to your tables via migrations. It's time to deploy your project to the Supabase Platform and start scaling up to millions of users! Head over to [Supabase](https://app.supabase.com) and create a new project to deploy to.
|
||||
|
||||
### Link your project
|
||||
|
||||
Associate your project with your remote project using [`supabase link`](/docs/reference/cli/usage#supabase-link).
|
||||
|
||||
```bash
|
||||
supabase link --project-ref <project-id>
|
||||
# You can get <project-id> from your project's dashboard URL: https://app.supabase.com/project/<project-id>
|
||||
|
||||
supabase db remote commit
|
||||
# Capture any changes that you have made to your remote database before you went through the steps above
|
||||
# If you have not made any changes to the remote database, skip this step
|
||||
```
|
||||
|
||||
`supabase/migrations` is now populated with a migration in `..._remote_commit.sql`.
|
||||
This migration captures any changes required for your local database to match the schema of your remote Supabase project.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
There are a few commands required to link your project. We are in the process of consolidating these commands into a single command. Bear with us!
|
||||
|
||||
</Admonition>
|
||||
|
||||
Associate your project with your remote project using [`supabase link`](/docs/reference/cli/usage#supabase-link).
|
||||
|
||||
```bash
|
||||
supabase link --project-ref <project-id>
|
||||
# You can get <project-id> from your project’s dashboard URL: https://app.supabase.com/project/<project-id>
|
||||
|
||||
supabase db remote commit
|
||||
# Capture any changes that you have made to your database before setting up the CLI
|
||||
```
|
||||
|
||||
`supabase/migrations` is now populated with a migration in `..._remote_commit.sql`.
|
||||
This migration captures any changes required for your local database to match the schema of your remote Supabase project.
|
||||
|
||||
### Deploy database changes
|
||||
|
||||
Deploy any local database migrations using [`db push`](/docs/reference/cli/usage#supabase-db-push):
|
||||
@@ -244,21 +397,23 @@ Deploy any local database migrations using [`db push`](/docs/reference/cli/usage
|
||||
supabase db push
|
||||
```
|
||||
|
||||
Visiting your live project on [Supabase](https://app.supabase.com), you'll see a new `employees` table, complete with the `department` column you added in the second migration above.
|
||||
|
||||
### Deploy Edge Functions
|
||||
|
||||
Deploy any Edge Functions using [`functions deploy`](/docs/reference/cli/usage#supabase-functions-deploy):
|
||||
If your project uses Edge Functions, you can deploy these using [`functions deploy`](/docs/reference/cli/usage#supabase-functions-deploy):
|
||||
|
||||
```sh
|
||||
supabase functions deploy <function_name>
|
||||
```
|
||||
|
||||
## Limitations
|
||||
## Limitations and considerations
|
||||
|
||||
The local development environment is not as feature-complete as the Supabase Platform. Here are some of the differences:
|
||||
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 (however you can access them through the Docker containers).
|
||||
- You cannot update your project settings in the Dashboard—this must be done using the CLI.
|
||||
- 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} />
|
||||
|
||||
|
||||
@@ -40,15 +40,19 @@ drop
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
To disable an extension call `drop extension`.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Enabling some extensions with `create extension <extension-name> with schema extensions` may lead to permission issues (e.g., `dblink`, `http`, `pg_cron`).
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Most extensions are installed under the `extensions` schema, which is accessible to public by default. To avoid namespace pollution, we do not recommend creating other entities in the `extensions` schema.
|
||||
|
||||
If you need to restrict user access to tables managed by extensions, we recommend creating a separate schema for installing that specific extension.
|
||||
|
||||
Some extensions can only be created under a specific schema, for eg. `postgis_tiger_geocoder` extension creates a schema named `tiger`. Before enabling such extensions, make sure you have not created a conflicting schema with the same name.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Full list of extensions
|
||||
|
||||
Supabase is pre-configured with over 50 extensions. You can also install your own SQL extensions directly in the database through our SQL editor.
|
||||
|
||||
@@ -54,8 +54,8 @@ Given a table
|
||||
```sql
|
||||
create table "Blog"(
|
||||
id serial primary key,
|
||||
name varchar(255) not null,
|
||||
description varchar(255),
|
||||
name text not null,
|
||||
description text,
|
||||
);
|
||||
|
||||
insert into "Blog"(name)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -227,6 +227,8 @@ final result = await client
|
||||
|
||||
### Search multiple columns
|
||||
|
||||
Right now there is no direct way to use JavaScript or Dart to search through multiple columns but you can do it by creating [computed columns](https://postgrest.org/en/stable/api.html#computed-virtual-columns) on the database.
|
||||
|
||||
To find all `books` where `description` or `title` contain the word `little`:
|
||||
|
||||
<Tabs
|
||||
@@ -247,6 +249,38 @@ where
|
||||
@@ to_tsquery('little');
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```sql
|
||||
create function title_description(books) returns text as $$
|
||||
select $1.title || ' ' || $1.description;
|
||||
$$ language sql immutable;
|
||||
```
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase
|
||||
.from('books')
|
||||
.select()
|
||||
.textSearch('title_description', `little`)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="dart" label="Dart">
|
||||
|
||||
```sql
|
||||
create function title_description(books) returns text as $$
|
||||
select $1.title || ' ' || $1.description;
|
||||
$$ language sql immutable;
|
||||
```
|
||||
|
||||
```dart
|
||||
final result = await client
|
||||
.from('books')
|
||||
.select()
|
||||
.textSearch('title_description', "little")
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="data" label="Data">
|
||||
|
||||
|
||||
@@ -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 Git',
|
||||
description: 'How to deploy Supabase Edge Functions with a CI / CD pipeline.',
|
||||
video: 'https://www.youtube.com/v/6OMVWiiycLs',
|
||||
}
|
||||
|
||||
+3
-3
@@ -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',
|
||||
}
|
||||
@@ -22,8 +22,8 @@ Supabase Edge Functions allow you to go beyond HTTP and can connect to your Post
|
||||
import * as postgres from 'https://deno.land/x/postgres@v0.14.2/mod.ts'
|
||||
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
|
||||
|
||||
// Get the connection string from the environment variable "DATABASE_URL"
|
||||
const databaseUrl = Deno.env.get('DATABASE_URL')!
|
||||
// Get the connection string from the environment variable "SUPABASE_DB_URL"
|
||||
const databaseUrl = Deno.env.get('SUPABASE_DB_URL')!
|
||||
|
||||
// Create a database pool with three connections that are lazily established
|
||||
const pool = new postgres.Pool(databaseUrl, 3, true)
|
||||
@@ -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',
|
||||
}
|
||||
@@ -50,7 +50,7 @@ This will register a Slash Command named `hello` that accepts a parameter named
|
||||
import { json, serve, validateRequest } from 'sift'
|
||||
// TweetNaCl is a cryptography library that we use to verify requests
|
||||
// from Discord.
|
||||
import nacl from 'nacl'
|
||||
import nacl from 'tweetnacl'
|
||||
|
||||
enum DiscordCommandType {
|
||||
Ping = 1,
|
||||
|
||||
@@ -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.',
|
||||
}
|
||||
|
||||
|
||||
@@ -128,8 +128,8 @@ Edge Functions supports `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, and `OPTIONS`.
|
||||
- Deno Deploy limitations
|
||||
- Deno does not support outgoing connections to ports `25`, `465`, and `587`.
|
||||
- Cannot read or write to File System
|
||||
- NPM modules are not supported.
|
||||
- Edge Functions
|
||||
- Local development - only one function at a time
|
||||
- Serving of HTML content is not supported (`GET` requests that return `text/html` will be rewritten to `text/plain`).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -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
|
||||
@@ -218,6 +218,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} />
|
||||
|
||||
@@ -55,7 +55,7 @@ We use this with our [pg_graphql](https://github.com/supabase/pg_graphql) extens
|
||||
- Official Docs: [postgrest.org](https://postgrest.org/)
|
||||
- Source code: [github.com/PostgREST/postgrest](https://github.com/PostgREST/postgrest)
|
||||
- License: [MIT](https://github.com/PostgREST/postgrest/blob/main/LICENSE)
|
||||
- Language: Haskel
|
||||
- Language: Haskell
|
||||
|
||||
### Realtime (API & multiplayer)
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ export const meta = {
|
||||
```sql SQL_EDITOR
|
||||
-- Create the table
|
||||
CREATE TABLE countries (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL
|
||||
);
|
||||
-- Insert some sample data into the table
|
||||
INSERT INTO countries (name) VALUES ('United States');
|
||||
@@ -41,14 +41,14 @@ export const meta = {
|
||||
|
||||
<StepHikeCompact.Details title="Create a React app">
|
||||
|
||||
Create a React app using the `create-react-app` command.
|
||||
Create a React app using a [Vite](https://vitejs.dev/guide/) template.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
npx create-react-app my-app
|
||||
npm create vite@latest my-app -- --template react
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
@@ -77,25 +77,42 @@ export const meta = {
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Query data from the app">
|
||||
|
||||
In `index.js`, create a Supabase client using your [Project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
|
||||
In `App.jsx`, create a Supabase client using your [Project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
|
||||
|
||||
Add a `getCountries` function to fetch the data and log the query result to the browser console.
|
||||
Add a `getCountries` function to fetch the data and display the query result to the page.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
|
||||
```js src/index.js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
```js src/App.jsx
|
||||
import { useEffect, useState } from "react";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>')
|
||||
const supabase = createClient("https://<project>.supabase.co", "<your-anon-key>");
|
||||
|
||||
async function getCountries() {
|
||||
const countries = await supabase.from('countries').select()
|
||||
console.log(countries)
|
||||
function App() {
|
||||
const [countries, setCountries] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
getCountries();
|
||||
}, []);
|
||||
|
||||
async function getCountries() {
|
||||
const { data } = await supabase.from("countries").select();
|
||||
setCountries(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{countries.map((country) => (
|
||||
<li key={country.name}>{country.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
getCountries()
|
||||
export default App;
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
@@ -105,14 +122,14 @@ export const meta = {
|
||||
<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 see the list of countries.
|
||||
Start the app, go to http://localhost:5173 in a browser, and open the browser console and you should see the list of countries.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash Terminal
|
||||
npm start
|
||||
npm run dev
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function Auth() {
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Input
|
||||
label="Email"
|
||||
@@ -234,7 +234,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>
|
||||
|
||||
@@ -22,11 +22,11 @@ Let's start building the React app from scratch.
|
||||
|
||||
### Initialize a React app
|
||||
|
||||
We can use [Create React App](https://create-react-app.dev/docs/getting-started/) to initialize
|
||||
We can use [Vite](https://vitejs.dev/guide/) to initialize
|
||||
an app called `supabase-react`:
|
||||
|
||||
```bash
|
||||
npx create-react-app supabase-react
|
||||
npm create vite@latest supabase-react -- --template react
|
||||
cd supabase-react
|
||||
```
|
||||
|
||||
@@ -40,8 +40,8 @@ And finally we want to save the environment variables in a `.env.local` file.
|
||||
All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
```bash title=.env
|
||||
REACT_APP_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
REACT_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
VITE_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
```
|
||||
|
||||
Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client. These variables will be exposed
|
||||
@@ -52,8 +52,8 @@ Create and edit `src/supabaseClient.js`:
|
||||
```js title=src/supabaseClient.js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL
|
||||
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
```
|
||||
@@ -65,9 +65,9 @@ You can find the full contents of this file [here](https://raw.githubusercontent
|
||||
|
||||
Let's set up a React component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
|
||||
|
||||
Create and edit `src/Auth.js`:
|
||||
Create and edit `src/Auth.jsx`:
|
||||
|
||||
```jsx title=src/Auth.js
|
||||
```jsx title=src/Auth.jsx
|
||||
import { useState } from 'react'
|
||||
import { supabase } from './supabaseClient'
|
||||
|
||||
@@ -75,44 +75,42 @@ export default function Auth() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
const handleLogin = async (e) => {
|
||||
e.preventDefault()
|
||||
const handleLogin = async (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const { error } = await supabase.auth.signInWithOtp({ email })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
setLoading(true)
|
||||
const { error } = await supabase.auth.signInWithOtp({ email })
|
||||
|
||||
if (error) {
|
||||
alert(error.error_description || error.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
} else {
|
||||
alert('Check your email for the login link!')
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row flex-center flex">
|
||||
<div className="col-6 form-widget" aria-live="polite">
|
||||
<div className="row flex flex-center">
|
||||
<div className="col-6 form-widget">
|
||||
<h1 className="header">Supabase + React</h1>
|
||||
<p className="description">Sign in via magic link with your email below</p>
|
||||
{loading ? (
|
||||
'Sending magic link...'
|
||||
) : (
|
||||
<form onSubmit={handleLogin}>
|
||||
<label htmlFor="email">Email</label>
|
||||
<form className="form-widget" onSubmit={handleLogin}>
|
||||
<div>
|
||||
<input
|
||||
id="email"
|
||||
className="inputField"
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
value={email}
|
||||
required={true}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<button className="button block" aria-live="polite">
|
||||
Send magic link
|
||||
</div>
|
||||
<div>
|
||||
<button className={'button block'} disabled={loading}>
|
||||
{loading ? <span>Loading</span> : <span>Send magic link</span>}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -123,130 +121,119 @@ export default function Auth() {
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
|
||||
Let's create a new component for that called `src/Account.js`.
|
||||
Let's create a new component for that called `src/Account.jsx`.
|
||||
|
||||
```jsx title=src/Account.js
|
||||
```jsx title=src/Account.jsx
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from './supabaseClient'
|
||||
|
||||
const Account = ({ session }) => {
|
||||
export default function Account({ session }) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [username, setUsername] = useState(null)
|
||||
const [website, setWebsite] = useState(null)
|
||||
const [avatar_url, setAvatarUrl] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
getProfile()
|
||||
}, [session])
|
||||
|
||||
const getProfile = async () => {
|
||||
try {
|
||||
async function getProfile() {
|
||||
setLoading(true)
|
||||
const { user } = session
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
let { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (data) {
|
||||
if (error) {
|
||||
console.warn(error)
|
||||
} else if (data) {
|
||||
setUsername(data.username)
|
||||
setWebsite(data.website)
|
||||
setAvatarUrl(data.avatar_url)
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const updateProfile = async (e) => {
|
||||
e.preventDefault()
|
||||
getProfile()
|
||||
}, [session])
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const { user } = session
|
||||
async function updateProfile(event) {
|
||||
event.preventDefault()
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date(),
|
||||
}
|
||||
setLoading(true)
|
||||
const { user } = session
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) {
|
||||
alert(error.message)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div aria-live="polite">
|
||||
{loading ? (
|
||||
'Saving ...'
|
||||
) : (
|
||||
<form onSubmit={updateProfile} className="form-widget">
|
||||
<div>Email: {session.user.email}</div>
|
||||
<div>
|
||||
<label htmlFor="username">Name</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="url"
|
||||
value={website || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button className="button primary block" disabled={loading}>
|
||||
Update profile
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
<button type="button" className="button block" onClick={() => supabase.auth.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={updateProfile} className="form-widget">
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="username">Name</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
required
|
||||
value={username || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="website"
|
||||
value={website || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button className="button block primary" type="submit" disabled={loading}>
|
||||
{loading ? 'Loading ...' : 'Update'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button className="button block" type="button" onClick={() => supabase.auth.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default Account
|
||||
```
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `src/App.js`:
|
||||
Now that we have all the components in place, let's update `src/App.jsx`:
|
||||
|
||||
```jsx title=src/App.js
|
||||
import './index.css'
|
||||
```jsx title=src/App.jsx
|
||||
import './App.css'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from './supabaseClient'
|
||||
import Auth from './Auth'
|
||||
import Account from './Account'
|
||||
|
||||
export default function App() {
|
||||
function App() {
|
||||
const [session, setSession] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -265,15 +252,17 @@ export default function App() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
```
|
||||
|
||||
Once that's done, run this in a terminal window:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
npm run dev
|
||||
```
|
||||
|
||||
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
|
||||
And then open the browser to [localhost:5173](http://localhost:5173) and you should see the completed app.
|
||||
|
||||

|
||||
|
||||
@@ -285,9 +274,9 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
|
||||
|
||||
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:
|
||||
|
||||
Create and edit `src/Avatar.js`:
|
||||
Create and edit `src/Avatar.jsx`:
|
||||
|
||||
```jsx title=src/Avatar.js
|
||||
```jsx title=src/Avatar.jsx
|
||||
import { useEffect, useState } from 'react'
|
||||
import { supabase } from './supabaseClient'
|
||||
|
||||
@@ -299,7 +288,7 @@ export default function Avatar({ url, size, onUpload }) {
|
||||
if (url) downloadImage(url)
|
||||
}, [url])
|
||||
|
||||
const downloadImage = async (path) => {
|
||||
async function downloadImage(path) {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
if (error) {
|
||||
@@ -312,7 +301,7 @@ export default function Avatar({ url, size, onUpload }) {
|
||||
}
|
||||
}
|
||||
|
||||
const uploadAvatar = async (event) => {
|
||||
async function uploadAvatar(event) {
|
||||
try {
|
||||
setUploading(true)
|
||||
|
||||
@@ -331,7 +320,7 @@ export default function Avatar({ url, size, onUpload }) {
|
||||
throw uploadError
|
||||
}
|
||||
|
||||
onUpload(filePath)
|
||||
onUpload(event, filePath)
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
@@ -340,31 +329,33 @@ export default function Avatar({ url, size, onUpload }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: size }} aria-live="polite">
|
||||
<img
|
||||
src={avatarUrl ? avatarUrl : `https://place-hold.it/${size}x${size}`}
|
||||
alt={avatarUrl ? 'Avatar' : 'No image'}
|
||||
className="avatar image"
|
||||
style={{ height: size, width: size }}
|
||||
/>
|
||||
{uploading ? (
|
||||
'Uploading...'
|
||||
<div>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt="Avatar"
|
||||
className="avatar image"
|
||||
style={{ height: size, width: size }}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<label className="button primary block" htmlFor="single">
|
||||
Upload an avatar
|
||||
</label>
|
||||
<div className="visually-hidden">
|
||||
<input
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
onChange={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<div className="avatar no-image" style={{ height: size, width: size }} />
|
||||
)}
|
||||
<div style={{ width: size }}>
|
||||
<label className="button primary block" htmlFor="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload'}
|
||||
</label>
|
||||
<input
|
||||
style={{
|
||||
visibility: 'hidden',
|
||||
position: 'absolute',
|
||||
}}
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
onChange={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -372,9 +363,9 @@ export default function Avatar({ url, size, onUpload }) {
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page at `src/Account.js`:
|
||||
And then we can add the widget to the Account page at `src/Account.jsx`:
|
||||
|
||||
```jsx title=src/Account.js
|
||||
```jsx title=src/Account.jsx
|
||||
// Import the new component
|
||||
import Avatar from './Avatar'
|
||||
|
||||
@@ -386,9 +377,9 @@ return (
|
||||
<Avatar
|
||||
url={avatar_url}
|
||||
size={150}
|
||||
onUpload={(url) => {
|
||||
onUpload={(event, url) => {
|
||||
setAvatarUrl(url)
|
||||
updateProfile({ username, website, avatar_url: url })
|
||||
updateProfile(event)
|
||||
}}
|
||||
/>
|
||||
{/* ... */}
|
||||
|
||||
@@ -45,16 +45,6 @@ PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
|
||||
PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"
|
||||
```
|
||||
|
||||
{/* Now that we have the API credentials in place, create a `src/lib/supabaseClient.ts` helper file to initialize the Supabase client.
|
||||
These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
|
||||
```js title=src/lib/supabaseClient.ts
|
||||
import { createClient } from '@supabase/auth-helpers-sveltekit'
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
|
||||
|
||||
export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY)
|
||||
``` */}
|
||||
|
||||
Optionally, add `src/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
|
||||
|
||||
### Supabase Auth Helpers
|
||||
@@ -116,12 +106,11 @@ If you are using TypeScript the compiler might complain about `event.locals.supa
|
||||
// src/app.d.ts
|
||||
|
||||
import { SupabaseClient, Session } from '@supabase/supabase-js';
|
||||
import { Database } from './DatabaseDefinitions';
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
supabase: SupabaseClient<Database>;
|
||||
supabase: SupabaseClient;
|
||||
getSession(): Promise<Session | null>;
|
||||
}
|
||||
interface PageData {
|
||||
@@ -133,57 +122,6 @@ declare global {
|
||||
}
|
||||
```
|
||||
|
||||
Update your `src/routes/+layout.svelte`:
|
||||
|
||||
```svelte title=src/routes/+layout.svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import '../styles.css';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>User Management</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<slot />
|
||||
</div>
|
||||
```
|
||||
|
||||
Create a new `src/routes/+layout.ts` file to handle the session and the supabase object on the client-side.
|
||||
|
||||
```ts title=src/routes/+layout.ts
|
||||
// src/routes/+layout.ts
|
||||
import { invalidate } from '$app/navigation';
|
||||
import {
|
||||
PUBLIC_SUPABASE_ANON_KEY,
|
||||
PUBLIC_SUPABASE_URL
|
||||
} from '$env/static/public';
|
||||
import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import type { Database } from '../DatabaseDefinitions';
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
depends('supabase:auth');
|
||||
|
||||
const supabase = createSupabaseLoadClient<Database>({
|
||||
supabaseUrl: PUBLIC_SUPABASE_URL,
|
||||
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
|
||||
event: { fetch },
|
||||
serverSession: data.session,
|
||||
onAuthStateChange() {
|
||||
invalidate('supabase:auth');
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
data: { session }
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
return { supabase, session };
|
||||
};
|
||||
```
|
||||
|
||||
Create a new `src/routes/+layout.server.ts` file to handle the session on the server-side.
|
||||
|
||||
```ts title=src/routes/+layout.server.ts
|
||||
@@ -197,6 +135,70 @@ export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
|
||||
};
|
||||
```
|
||||
|
||||
> Start your dev server (`npm run dev`) in order to generate the `./$types` files we are referencing in our project.
|
||||
|
||||
Create a new `src/routes/+layout.ts` file to handle the session and the supabase object on the client-side.
|
||||
|
||||
```ts title=src/routes/+layout.ts
|
||||
// src/routes/+layout.ts
|
||||
import { invalidate } from '$app/navigation';
|
||||
import {
|
||||
PUBLIC_SUPABASE_ANON_KEY,
|
||||
PUBLIC_SUPABASE_URL
|
||||
} from '$env/static/public';
|
||||
import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
depends('supabase:auth');
|
||||
|
||||
const supabase = createSupabaseLoadClient({
|
||||
supabaseUrl: PUBLIC_SUPABASE_URL,
|
||||
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
|
||||
event: { fetch },
|
||||
serverSession: data.session
|
||||
});
|
||||
|
||||
const {
|
||||
data: { session }
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
return { supabase, session };
|
||||
};
|
||||
```
|
||||
|
||||
Update your `src/routes/+layout.svelte`:
|
||||
|
||||
```svelte title=src/routes/+layout.svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import '../styles.css';
|
||||
import { invalidate } from '$app/navigation'
|
||||
import { onMount } from 'svelte'
|
||||
import type { LayoutData } from './$types'
|
||||
|
||||
export let data: LayoutData
|
||||
|
||||
$: ({ supabase } = data)
|
||||
|
||||
onMount(() => {
|
||||
const { data } = supabase.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth')
|
||||
})
|
||||
|
||||
return () => data.subscription.unsubscribe()
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>User Management</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<slot />
|
||||
</div>
|
||||
```
|
||||
|
||||
### Set up a Login page
|
||||
|
||||
#### Supabase Auth UI
|
||||
@@ -249,10 +251,10 @@ and send it over to the server. This all happens behind the scenes and isn't som
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent, url }) => {
|
||||
const { session } = await parent();
|
||||
export const load: PageServerLoad = async ({ url, locals: { getSession } }) => {
|
||||
const session = await getSession();
|
||||
|
||||
// if the user is already logged in return them to the account page
|
||||
// if the user is already logged in return them to the account page
|
||||
if (session) {
|
||||
throw redirect(303, '/account');
|
||||
}
|
||||
@@ -558,7 +560,7 @@ And then we can add the widget to the Account page:
|
||||
|
||||
```svelte title=src/routes/account/+page.svelte
|
||||
<!-- src/routes/account/+page.svelte -->
|
||||
<script>
|
||||
<script lang="ts">
|
||||
// Import the new component
|
||||
import Avatar from './Avatar.svelte'
|
||||
</script>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -162,7 +162,7 @@ Update your Prisma schema by setting the `directUrl` in the datasource block:
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directURL = env("DIRECT_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ Remove the default columns
|
||||
|
||||
Create two new columns:
|
||||
|
||||
- `user_id` as `varchar` as primary key
|
||||
- `email` as `varchar`
|
||||
- `user_id` as `text` as primary key
|
||||
- `email` as `text`
|
||||
|
||||
Click `Save` to create the new table.
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ If you haven't already, create a new project in Supabase and create a table call
|
||||
|
||||
- id (int8)
|
||||
- created_at (timestamptz)
|
||||
- make (varchar)
|
||||
- model (varchar)
|
||||
- make (text)
|
||||
- model (text)
|
||||
- year (int8)
|
||||
- rating (int2)
|
||||
- author (varchar)
|
||||
- author (text)
|
||||
|
||||
Manually enter a couple of rows of data, so that we have something to read from the DB.
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ Database backups are an integral part of any disaster recovery plan. Disasters c
|
||||
|
||||
## Frequency of Backups
|
||||
|
||||
When deciding how often a database should be backed up, the key business metric Recovery Point Objective (RPO) should be considered. RPO is the threshold for how much data, measured in time, a business could lose when disaster strikes. This amount is fully dependent on a business and its underlying requirements. A low RPO would mean that database backups would have to be taken at an increased cadence throughout the day. Each Supabase project has access to two forms of backups, Daily Backups and Point-in-Time Recovery. The agreed upon RPO would be a deciding factor in choosing which solution best fits a project.
|
||||
When deciding how often a database should be backed up, the key business metric Recovery Point Objective (RPO) should be considered. RPO is the threshold for how much data, measured in time, a business could lose when disaster strikes. This amount is fully dependent on a business and its underlying requirements. A low RPO would mean that database backups would have to be taken at an increased cadence throughout the day. Each Supabase project has access to two forms of backups, Daily Backups and Point-in-Time Recovery (PITR). The agreed upon RPO would be a deciding factor in choosing which solution best fits a project.
|
||||
|
||||
Daily Backups and PITR are mutually exclusive. If your project opts into using PITR, Daily Backups will no longer be taken. They're also unnecessary, as PITR supports a superset of functionality, in terms of the granular recovery that can be performed.
|
||||
|
||||
<Admonition type="note">
|
||||
Database backups do not include objects stored via the Storage API, as the database only includes
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -60,6 +60,12 @@ Supabase provides a logging interface specific to each product. You can use simp
|
||||
|
||||
[Realtime logs](https://app.supabase.com/project/_/logs/realtime-logs) show all server logs for your [Realtime API usage](../../guides/realtime).
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Realtime connections are not logged by default. Turn on [Realtime connection logs per client](#logging-realtime-connections) with the `log_level` parameter.
|
||||
|
||||
</Admonition>
|
||||
|
||||

|
||||
|
||||
</TabPanel>
|
||||
@@ -126,6 +132,21 @@ If any permission errors are encountered when executing `alter role postgres ...
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Logging Realtime Connections
|
||||
|
||||
Realtime doesn't log new WebSocket connections or Channel joins by default. Enable connection logging per client by including an `info` `log_level` parameter when instantiating the Supabase client.
|
||||
|
||||
```javascript
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const options = {
|
||||
realtime: {
|
||||
log_level: 'info',
|
||||
},
|
||||
}
|
||||
const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key', options)
|
||||
```
|
||||
|
||||
## Logs Explorer
|
||||
|
||||
The [Logs Explorer](https://app.supabase.com/project/_/logs-explorer) exposes logs from each part of the Supabase stack as a separate table that can be queried and joined using SQL.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Spend cap',
|
||||
description: 'Learn about the cost control of Supabase.',
|
||||
}
|
||||
|
||||
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.
|
||||
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?
|
||||
|
||||
Supabase charges for certain usage line items in your project. Those line items include:
|
||||
|
||||
- Database Size
|
||||
- Storage Size
|
||||
- Egress
|
||||
- Asset transformations
|
||||
- Realtime concurrent peak connections
|
||||
- Realtime messages
|
||||
- Edge function invocations
|
||||
- Number of Edge functions
|
||||
|
||||
You can view the pricing, and free quota allowance of each line item on the [pricing page](/pricing).
|
||||
|
||||
You can monitor your usage and costs using the [Usage section of your project](https://app.supabase.com/project/_/settings/billing/usage) in the dashboard.
|
||||
This will show you how much you have spent so far in the current billing cycle, as well as an estimate of your expected total cost for the cycle based on your current usage.
|
||||
|
||||
## How can I turn the Spend Cap on or off?
|
||||
|
||||
To set up a Spend Cap in your project, navigate to the [Subscription section](https://app.supabase.com/project/_/settings/billing/subscription) of your project.
|
||||
From there, you can click on "Change subscription" to select the plan you want to use and, at the same time, turn on or off the Spend Cap.
|
||||
|
||||
## 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.
|
||||
|
||||
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).
|
||||
This additional usage will be charged based on the line item's cost per unit outlined on the [pricing page](/pricing).
|
||||
|
||||
<Admonition type="note">
|
||||
When your Spend Cap is **off**, we recommend monitoring your consumption and costs using the [Usage
|
||||
section of your project](https://app.supabase.com/project/_/settings/billing/usage) in the
|
||||
dashboard.
|
||||
</Admonition>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,58 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'architecture',
|
||||
title: 'Realtime Architecture',
|
||||
description: 'Architecture of the Supabase Realtime service',
|
||||
sidebar_label: 'Architecture',
|
||||
}
|
||||
|
||||
Realtime is a globally distributed Elixir cluster. Clients can connect to any node in the cluster via WebSockets and send messages to any other client connected to the cluster.
|
||||
|
||||
Realtime is written in [Elixir](https://elixir-lang.org/), which compiles to [Erlang](https://www.erlang.org/), and utilizes many tools the [Phoenix Framework](https://www.phoenixframework.org/) provides out of the box.
|
||||
|
||||

|
||||
|
||||
## Elixir & Phoenix
|
||||
|
||||
Phoenix is fast and able to handle millions of concurrent connections.
|
||||
|
||||
Phoenix can handle many concurrent connections because Elixir provides lightweight processes (not OS processes) to work with.
|
||||
|
||||
Client-facing WebSocket servers need to handle many concurrent connections. Elixir & Phoenix let the Supabase Realtime cluster do this easily.
|
||||
|
||||
## Global Cluster
|
||||
|
||||
Presence is an in-memory key-value store backed by a CRDT. When a user is connected to the cluster the state of that user is sent to all connected Realtime nodes.
|
||||
|
||||
Broadcast lets you send a message from any connected client to a Channel. Any other client connected to that same Channel will receive that message.
|
||||
|
||||
This works globally. A client connected to a Realtime node in the United States can send a message to another client connected to a node in Singapore. Simply connect two clients to the same Realtime Channel and they'll all receive the same messages.
|
||||
|
||||
Broadcast is useful for getting messages to users in the same location very quickly. If a group of clients are connected to a node in Singapore, the message only needs to go to that Realtime node in Singapore and back down. If users are close to a Realtime node they'll get Broadcast messages in the time it takes to ping the cluster.
|
||||
|
||||
Thanks to the Realtime cluster, you (an amazing Supabase user) don't have to think about which regions your clients are connected to.
|
||||
|
||||
If you're using Broadcast, Presence, or streaming database changes, messages will always get to your users via the shortest path possible.
|
||||
|
||||
## Connecting to a Database
|
||||
|
||||
Realtime allows you to listen to changes from your Postgres database. When a new client connects to Realtime and initializes the `postgres_changes` Realtime Extension the cluster will connect to your Postgres database and start streaming changes from a replication slot.
|
||||
|
||||
Realtime knows the region your database is in, and connects to it from the closest region possible.
|
||||
|
||||
Every Realtime region has at least two nodes so if one node goes offline the other node should reconnect and start streaming changes again.
|
||||
|
||||
## Streaming the Write-Ahead Log
|
||||
|
||||
A Postgres logical replication slot is acquired when connecting to your database.
|
||||
|
||||
Realtime delivers changes by polling the replication slot and appending channel subscription IDs to each wal record.
|
||||
|
||||
Subscription IDs are Erlang processes representing underlying sockets on the cluster. These IDs are globally unique and messages to processes are routed automatically by the Erlang virtual machine.
|
||||
|
||||
After receiving results from the polling query, with subscription IDs appended, Realtime delivers records to those clients.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -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
|
||||
@@ -0,0 +1,22 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'channels',
|
||||
title: 'Realtime Channels',
|
||||
description: 'WebSockets are Channels on Supabase Realtime',
|
||||
sidebar_label: 'Channels',
|
||||
}
|
||||
|
||||
You can uniquely reference a channel by its `topic` you define when you initialize your Supabase Realtime client. Everyone connected to the same Channel topic will receive the same messages.
|
||||
|
||||
Clients can send and receive messages bi-directionally over a Channel. The Realtime backend can also push messages to all clients connected to the same Channel.
|
||||
|
||||
A single client can receive change records from Postgres, Broadcast messages from other clients and Presence updates all over the same Channel.
|
||||
|
||||
Channels are implemented over [Phoenix Channels](https://hexdocs.pm/phoenix/channels.html) which uses [Phoenix.PubSub](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html) with the default `Phoenix.PubSub.PG2` adapter.
|
||||
|
||||
The PG2 adapter utilizes Erlang [process groups](https://www.erlang.org/docs/18/man/pg2.html) to implement the PubSub model where a publisher can send messages to many subscribers.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,27 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'extensions',
|
||||
title: 'Realtime Extensions',
|
||||
description:
|
||||
'Extensions enable powerful Realtime features like Presence, Broadcast and database change streaming',
|
||||
sidebar_label: 'Extensions',
|
||||
}
|
||||
|
||||
Supabase Realtime provides multiplayer features so that you can build real-time applications. The server has several built-in extensions to solve specific tasks.
|
||||
|
||||
- [Broadcast](/docs/guides/realtime/broadcast): for sending rapid, ephemeral messages to other connected clients. A typical use-case is tracking mouse movements.
|
||||
- [Presence](/docs/guides/realtime/presence): for sending user state between connected clients. A typical use-case is simply an "online" status, which will disappear when a user is disconnected.
|
||||
- [Postgres Changes](/docs/guides/realtime/postgres-changes): for receiving database changes in real-time.
|
||||
|
||||
## Choosing between Broadcast and Presence
|
||||
|
||||
Defaulting to using Broadcast is a good idea, and then use Presence sparingly. The Presence extension merges all changes into a shared state for every client connected to the same "channel". If you _do_ use Presence, it's best to throttle your changes so that you are sending updates less frequently.
|
||||
|
||||
## Future possibilities
|
||||
|
||||
Realtime extensions are still under development. We have many more extensions planned. You can follow our progress by watching the [Realtime](https://github.com/supabase/realtime) repo on GitHub.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
File renamed without changes.
+20
-7
@@ -15,7 +15,9 @@ Clients can choose to receive `INSERT`, `UPDATE`, `DELETE`, or `*` (all) changes
|
||||
Postgres Changes works out of the box for tables in the `public` schema. You can listen to tables in your private schemas by granting table `SELECT` permissions to the database role found in your access token. You can run a query similar to the following:
|
||||
|
||||
```sql
|
||||
GRANT SELECT ON "private_schema"."table" TO authenticated;
|
||||
grant
|
||||
select
|
||||
on "private_schema"."table" to authenticated;
|
||||
```
|
||||
|
||||
<Admonition type="caution">
|
||||
@@ -29,15 +31,19 @@ You can do this in the [Replication](https://app.supabase.com/project/_/database
|
||||
|
||||
```sql
|
||||
begin;
|
||||
-- remove the supabase_realtime publication
|
||||
drop publication if exists supabase_realtime;
|
||||
|
||||
-- re-create the supabase_realtime publication with no tables
|
||||
create publication supabase_realtime;
|
||||
-- remove the supabase_realtime publication
|
||||
drop
|
||||
publication if exists supabase_realtime;
|
||||
|
||||
-- re-create the supabase_realtime publication with no tables
|
||||
create publication supabase_realtime;
|
||||
|
||||
commit;
|
||||
|
||||
-- add a table to the publication
|
||||
alter publication supabase_realtime add table messages;
|
||||
alter
|
||||
publication supabase_realtime add table messages;
|
||||
```
|
||||
|
||||
### Full `old` Record
|
||||
@@ -46,9 +52,16 @@ By default, only `new` record changes are sent but if you want to receive the `o
|
||||
you can set the `replica identity` of your table to `full`:
|
||||
|
||||
```sql
|
||||
alter table messages replica identity full;
|
||||
alter table
|
||||
messages replica identity full;
|
||||
```
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
RLS policies are not applied to `DELETE` statements. When RLS is enabled and `replica identity` is set to `full` on a table, the `old` record contains only the primary key(s).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Schema Changes
|
||||
|
||||
To listen to all changes in the `public` schema:
|
||||
File renamed without changes.
@@ -0,0 +1,194 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'protocol',
|
||||
title: 'Realtime Protocol',
|
||||
description: 'Understanding Realtime Protocol',
|
||||
}
|
||||
|
||||
The Realtime Protocol is a set of message formats used for communication over a WebSocket connection between a Realtime client and server. These messages are used to initiate a connection, update access tokens, receive system status updates, and receive real-time updates from the Postgres database.
|
||||
|
||||
## Connection
|
||||
|
||||
In the initial message, the client sends a message specifying the features they want to use (Broadcast, Presence, Postgres Changes).
|
||||
|
||||
```ts
|
||||
{
|
||||
"event": "phx_join",
|
||||
"topic": string,
|
||||
"payload": {
|
||||
"config": {
|
||||
"broadcast": {
|
||||
"self": boolean
|
||||
},
|
||||
"presence": {
|
||||
"key": string
|
||||
},
|
||||
"postgres_changes": [
|
||||
{
|
||||
"event": "*" | "INSERT" | "UPDATE" | "DELETE",
|
||||
"schema": string,
|
||||
"table": string,
|
||||
"filter": string + '=' + "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" + '.' + string
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"ref": string
|
||||
}
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
The `in` filter has the format `COLUMN_NAME=in.(value1,value2,value3)`. However, other filters use the format `COLUMN_NAME=FILTER_NAME.value`.
|
||||
</Admonition>
|
||||
|
||||
In response, the server sends the Postgres configuration with a unique ID. With this ID, the client should route incoming changes to the appropriate callback.
|
||||
|
||||
```ts
|
||||
{
|
||||
"event": "phx_reply",
|
||||
"topic": string,
|
||||
"payload": {
|
||||
"response": {
|
||||
"postgres_changes": [
|
||||
{
|
||||
"id": number,
|
||||
"event": "*" | "INSERT" | "UPDATE" | "DELETE",
|
||||
"schema": string,
|
||||
"table": string,
|
||||
"filter": string + '=' + "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" + '.' + string
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "ok" | "error"
|
||||
},
|
||||
"ref": string
|
||||
}
|
||||
```
|
||||
|
||||
## System messages
|
||||
|
||||
System message are used to inform a client about the status of the Postgres subscription. The `payload.status` indicates if the subscription successful or not.
|
||||
The body of the `payload.message` can be "Subscribed to PostgreSQL" or "Subscribing to PostgreSQL failed" with subscription params.
|
||||
|
||||
```ts
|
||||
{
|
||||
"event": "system",
|
||||
"topic": string,
|
||||
"payload":{
|
||||
"channel": string,
|
||||
"extension": "postgres_changes",
|
||||
"message": "Subscribed to PostgreSQL" | "Subscribing to PostgreSQL failed",
|
||||
"status": "ok" | "error"
|
||||
},
|
||||
"ref": null,
|
||||
}
|
||||
```
|
||||
|
||||
## Heartbeat
|
||||
|
||||
The heartbeat message should be sent every 30 seconds to avoid a connection timeout.
|
||||
|
||||
```ts
|
||||
{
|
||||
"event": "heartbeat",
|
||||
"topic": "phoenix",
|
||||
"payload": {},
|
||||
"ref": string
|
||||
}
|
||||
```
|
||||
|
||||
## Access Token
|
||||
|
||||
To update the access token, you need to send to the server a message specifying a new token in the `payload.access_token` value.
|
||||
|
||||
```ts
|
||||
{
|
||||
"event": "access_token",
|
||||
"topic": string,
|
||||
"payload":{
|
||||
"access_token": string
|
||||
},
|
||||
"ref": string
|
||||
}
|
||||
```
|
||||
|
||||
## Postgres CDC message
|
||||
|
||||
Realtime sends a message with the following structure
|
||||
|
||||
```ts
|
||||
{
|
||||
"event": "postgres_changes",
|
||||
"topic": string,
|
||||
"payload": {
|
||||
"data": {
|
||||
"columns": Array<{name: string, type: string}>,
|
||||
"commit_timestamp": string,
|
||||
"errors": null | string,
|
||||
"old_record": {"id": number | string},
|
||||
"record": {[key: string]: boolean | number | string | null},
|
||||
"type": "*" | "INSERT" | "UPDATE" | "DELETE",
|
||||
"schema": string,
|
||||
"table": string
|
||||
},
|
||||
"ids": Array<number>
|
||||
},
|
||||
"ref": null
|
||||
}
|
||||
```
|
||||
|
||||
## Broadcast message
|
||||
|
||||
Structure of the broadcast event
|
||||
|
||||
```ts
|
||||
{
|
||||
"event": "broadcast",
|
||||
"topic": string,
|
||||
"payload": {
|
||||
"event": string,
|
||||
"payload": {[key: string]: boolean | number | string | null | undefined},
|
||||
"type": "broadcast"
|
||||
},
|
||||
"ref": null
|
||||
}
|
||||
```
|
||||
|
||||
## Presence message
|
||||
|
||||
The Presence events allow clients to monitor the online status of other clients in real-time.
|
||||
|
||||
### State Update
|
||||
|
||||
After joining, the server sends a `presence_state` message to a client with presence information. The payload field contains keys in UUID format, where each key represents a client and its value is a JSON object containing information about that client.
|
||||
|
||||
```ts
|
||||
{
|
||||
"event": "presence_state",
|
||||
"topic": string,
|
||||
"payload": {
|
||||
[key: string]: {metas: Array<{phx_ref: string, name: string, t: float}>}
|
||||
},
|
||||
"ref": null
|
||||
}
|
||||
```
|
||||
|
||||
### Diff Update
|
||||
|
||||
After a change to the presence state, such as a client joining or leaving, the server sends a presence_diff message to update the client's view of the presence state. The payload field contains two keys, `joins` and `leaves`, which represent clients that have joined and left, respectively. The values associated with each key are UUIDs of the clients.
|
||||
|
||||
```ts
|
||||
{
|
||||
"event": "presence_diff",
|
||||
"topic": string,
|
||||
"payload": {
|
||||
"joins": {metas: Array<{phx_ref: string, name: string, t: float}>},
|
||||
"leaves": {metas: Array<{phx_ref: string, name: string, t: float}>}
|
||||
},
|
||||
"ref": null
|
||||
}
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
export default Page
|
||||
@@ -0,0 +1,24 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'realtime-with-nextjs',
|
||||
title: 'Using Realtime with Next.js',
|
||||
description: 'Client & Server Components in Next.js with Realtime Updates',
|
||||
sidebar_label: 'Videos',
|
||||
}
|
||||
|
||||
In this guide we explore the best ways to receive realtime Postgres changes with your Next.js application.
|
||||
We'll show both client and serverside updates, and explore the which option is best.
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/YR-xP6PPXXA"
|
||||
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} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,80 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'subscribing-to-database-changes',
|
||||
title: 'Subscribing to Database Changes',
|
||||
description: 'Listen to database changes in real-time from your website or application.',
|
||||
sidebar_label: 'Videos',
|
||||
}
|
||||
|
||||
Supabase allows you to subscribe to real-time changes on your database from your client application.
|
||||
|
||||
You can listen to database changes using the [Postgres Changes](/docs/guides/realtime/postgres-changes) extension.
|
||||
|
||||
## Demo
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/2rUjcmgZDwQ"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
## Streaming inserts
|
||||
|
||||
You can use the `INSERT` event to stream all new rows.
|
||||
|
||||
```js
|
||||
const { createClient } = require('@supabase/supabase-js')
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
||||
|
||||
const channel = supabase
|
||||
.channel('schema-db-changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
},
|
||||
(payload) => console.log(payload)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
## Streaming updates
|
||||
|
||||
You can use the `UPDATE` event to stream all updated rows.
|
||||
|
||||
```js
|
||||
const { createClient } = require('@supabase/supabase-js')
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
||||
|
||||
const channel = supabase
|
||||
.channel('schema-db-changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
},
|
||||
(payload) => console.log(payload)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
## More resources
|
||||
|
||||
- Learn more about the [Postgres Changes](/docs/guides/realtime/postgres-changes) extension.
|
||||
- Client Libraries:
|
||||
- [JavaScript](/docs/reference/javascript/subscribe)
|
||||
- [Flutter](/docs/reference/dart/stream)
|
||||
- [Python](/docs/reference/python/subscribe)
|
||||
- [C#](/docs/reference/csharp/subscribe)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -64,7 +64,7 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
|
||||
- [Svelte Todo List.](https://github.com/supabase/supabase/tree/master/examples/todo-list/sveltejs-todo-list)
|
||||
- [Vue 3 Todo List (Typescript).](https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/vue3-ts-todo-list)
|
||||
- [Angular Todo List.](https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/angular-todo-list)
|
||||
- [Nuxt 3 Todo List.](https://github.com/nuxt-community/supabase-module/tree/main/demo)
|
||||
- [Nuxt 3 Todo List.](https://github.com/nuxt-modules/supabase/tree/main/demo)
|
||||
|
||||
### Auth examples
|
||||
|
||||
@@ -92,8 +92,8 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
|
||||
- `useSupabase`: Supabase React Hooks. [GitHub](https://github.com/gbibeaul/use-supabase)
|
||||
- `vue-supabase`: Supabase Vue wrapper. [GitHub](https://github.com/supabase/vue-supabase)
|
||||
- `vue-3-supabase`: Supabase Vue 3 wrapper. [GitHub](https://github.com/DidoMarchet/vue-3-supabase)
|
||||
- `nuxt-supabase`: Supabase Nuxt wrapper. [GitHub](https://github.com/supabase/nuxt-supabase)
|
||||
- `@nuxtjs/supabase`: Supabase Nuxt 3 module. [GitHub](https://github.com/nuxt-community/supabase-module)
|
||||
- `nuxt-supabase`: Supabase Nuxt wrapper. [GitHub](https://github.com/supabase-community/nuxt-supabase)
|
||||
- `@nuxtjs/supabase`: Supabase Nuxt 3 module. [GitHub](https://github.com/nuxt-modules/supabase)
|
||||
- `react-supabase`: Supabase React Hooks. [Docs](https://react-supabase.vercel.app) [GitHub](https://github.com/tmm/react-supabase)
|
||||
- ` @triniwiz/nativescript-supabase`: Supabase NativeScript. [GitHub](https://github.com/triniwiz/nativescript-plugins/tree/master/packages/nativescript-supabase)
|
||||
|
||||
@@ -133,10 +133,11 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
|
||||
- Supabase + Nuxt3 (Nuxt Starter Kit) [GitHub](https://github.com/one-aalam/nuxt-starter-kit)
|
||||
- Supabase + Remix (Remix Starter Kit) [GitHub](https://github.com/one-aalam/remix-starter-kit)
|
||||
- Supabase + Angular (Angular Starter Kit) [GitHub](https://github.com/one-aalam/ng-starter-kit)
|
||||
- Supabase + Nuxt3 + nuxtjs/supabase [Github](https://github.com/nuxt-community/supabase-module/tree/main/demo)
|
||||
- Supabase + Nuxt3 + nuxtjs/supabase [Github](https://github.com/nuxt-modules/supabase/tree/main/demo)
|
||||
- Supabase + Ory Kratos & Ory Oathkeeper [Github](https://github.com/ory/examples/tree/master/kratos-keto-oathkeeper-supabase)
|
||||
- Supabase + Ory Cloud [Github](https://github.com/ory/examples/tree/master/supabase-ory-cloud)
|
||||
- Supabase Auth with React Native + Next.js (Monorepo) [Github](https://github.com/mateoguzmana/react-native-next-supabase-auth-monorepo)
|
||||
- Supabase Auth with Nuxt3 [Github](https://github.com/zackha/supaAuth)
|
||||
|
||||
### Blog Posts
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ export const meta = {
|
||||
|
||||
<div>{spec.info.description}</div>
|
||||
|
||||
See [this guide](/docs/guides/realtime/bring-your-own-database) if you want to configure your database to work with Realtime.
|
||||
|
||||
<div>
|
||||
{spec.info.tags.map(tag => {
|
||||
return (
|
||||
|
||||
@@ -7,9 +7,7 @@ export const meta = {
|
||||
sidebar_label: 'Quickstart',
|
||||
}
|
||||
|
||||
This guide shows the basic functionality of Supabase Storage. Find a full [example application on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management) or deploy it with Vercel for a preview:
|
||||
|
||||
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fuser-management%2Fnextjs-ts-user-management&project-name=supabase-user-management&repository-name=supabase-user-management&demo-title=Supabase%20User%20Management&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-nextjs-ts-user-management.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FZ3HkQqe.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=nextjs-user-management)
|
||||
This guide shows the basic functionality of Supabase Storage. Find a full [example application on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management) or deploy it with [Vercel for a preview](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fuser-management%2Fnextjs-ts-user-management&project-name=supabase-user-management&repository-name=supabase-user-management&demo-title=Supabase%20User%20Management&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-nextjs-ts-user-management.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FZ3HkQqe.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=nextjs-user-management).
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { CodeBlock, Tabs } from 'ui'
|
||||
import specFile from '~/../../spec/transforms/auth_v1_openapi_deparsed.json' assert { type: 'json' }
|
||||
import { gen_v3, enrichedOperation } from '~/lib/refGenerator/helpers'
|
||||
import { Tabs } from '~/../../packages/ui'
|
||||
import { gen_v3 } from '~/lib/refGenerator/helpers'
|
||||
|
||||
// @ts-ignore
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
import RefSubLayout from '~/layouts/ref/RefSubLayout'
|
||||
|
||||
export type AcceptedValue = {
|
||||
|
||||
@@ -2,67 +2,73 @@
|
||||
import specFile from '~/../../spec/gotrue_v1_config.yaml' assert { type: 'yml' }
|
||||
import { Parameter } from '~/lib/refGenerator/refTypes'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import Head from 'next/head'
|
||||
|
||||
// Parameters are grouped on the page by tag
|
||||
const TAGS = ['general']
|
||||
|
||||
export default function Config() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex my-16">
|
||||
<div className="w-full prose">
|
||||
<h1 className="text-4xl mb-16">{specFile.info.title} Configuration</h1>
|
||||
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
|
||||
<div>
|
||||
{TAGS.map((tag) =>
|
||||
specFile.parameters
|
||||
.filter((param: Parameter) => param.tags[0] === tag)
|
||||
.map((parameter: Parameter, index) => (
|
||||
<div>
|
||||
{index === 0 && <h2 className="text-xl capitalize">{tag}</h2>}
|
||||
<div className="mt-8">
|
||||
<div>
|
||||
<h2 className="text-xl font-medium text-scale-1200 font-mono">
|
||||
<span className="mr-2">$</span>
|
||||
{parameter.title}
|
||||
</h2>
|
||||
<div className="grid" id={parameter.id}>
|
||||
<div className="border-b pb-8" key={parameter.id}>
|
||||
<div className=" mb-16">
|
||||
<p className="mb-4 scroll-mt-16 mt-0 text-scale-1100 text-base">
|
||||
<ReactMarkdown>{parameter.description}</ReactMarkdown>
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2">
|
||||
Required: <code>{parameter.required.toString()}</code>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
Default:
|
||||
<code>
|
||||
{parameter.default ? parameter.default.toString() : 'None'}
|
||||
</code>
|
||||
<>
|
||||
<Head>
|
||||
<title>Auth config</title>
|
||||
</Head>
|
||||
<div>
|
||||
<div className="flex my-16">
|
||||
<div className="w-full prose">
|
||||
<h1 className="text-4xl mb-16">{specFile.info.title} Configuration</h1>
|
||||
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
|
||||
<div>
|
||||
{TAGS.map((tag) =>
|
||||
specFile.parameters
|
||||
.filter((param: Parameter) => param.tags[0] === tag)
|
||||
.map((parameter: Parameter, index) => (
|
||||
<div>
|
||||
{index === 0 && <h2 className="text-xl capitalize">{tag}</h2>}
|
||||
<div className="mt-8">
|
||||
<div>
|
||||
<h2 className="text-xl font-medium text-scale-1200 font-mono">
|
||||
<span className="mr-2">$</span>
|
||||
{parameter.title}
|
||||
</h2>
|
||||
<div className="grid" id={parameter.id}>
|
||||
<div className="border-b pb-8" key={parameter.id}>
|
||||
<div className=" mb-16">
|
||||
<p className="mb-4 scroll-mt-16 mt-0 text-scale-1100 text-base">
|
||||
<ReactMarkdown>{parameter.description}</ReactMarkdown>
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2">
|
||||
Required: <code>{parameter.required.toString()}</code>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
Default:
|
||||
<code>
|
||||
{parameter.default ? parameter.default.toString() : 'None'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{parameter.links &&
|
||||
parameter.links.map((link) => (
|
||||
<div>
|
||||
<h3>See also:</h3>
|
||||
<li>
|
||||
<a href={link.link}>{link.name}</a>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{parameter.links &&
|
||||
parameter.links.map((link) => (
|
||||
<div>
|
||||
<h3>See also:</h3>
|
||||
<li>
|
||||
<a href={link.link}>{link.name}</a>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Parameter } from '~/lib/refGenerator/refTypes'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import { Heading } from '~/components/CustomHTMLElements'
|
||||
import Head from 'next/head'
|
||||
|
||||
// Parameters are grouped on the page by tag
|
||||
const TAGS = ['general', 'auth', 'api', 'database', 'dashboard', 'local', 'edge-functions']
|
||||
@@ -21,69 +22,74 @@ const tocList = TAGS.map((tag) =>
|
||||
|
||||
export default function Config() {
|
||||
return (
|
||||
<div className="grid grid-cols-12 relative gap-4">
|
||||
<div className="relative col-span-12 md:col-span-9 transition-all ease-out duration-100">
|
||||
<div className="w-full prose">
|
||||
<h1 className="">CLI configuration</h1>
|
||||
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-800 to-brand-900 my-8"></div>
|
||||
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
|
||||
<div>
|
||||
{TAGS.map((tag) =>
|
||||
specFile.parameters
|
||||
.filter((param: Parameter) => param.tags[0] === tag)
|
||||
.map((parameter: Parameter, index) => (
|
||||
<div>
|
||||
{index === 0 && <h2 className="text-xl capitalize">{tag}</h2>}
|
||||
<div className="mt-8">
|
||||
<div>
|
||||
<Heading tag="h2" parseAnchors={false} customAnchor={parameter.id}>
|
||||
<code>{parameter.title}</code>
|
||||
</Heading>
|
||||
<div className="grid">
|
||||
<div className="border-b pb-8" key={parameter.id}>
|
||||
<div className=" mb-16">
|
||||
<p className="mb-4 scroll-mt-16 mt-0 text-scale-1100 text-base">
|
||||
<ReactMarkdown>{parameter.description}</ReactMarkdown>
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2">
|
||||
Required: <code>{parameter.required.toString()}</code>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
Default:
|
||||
<code>
|
||||
{parameter.default ? parameter.default.toString() : 'None'}
|
||||
</code>
|
||||
<>
|
||||
<Head>
|
||||
<title>Supabase CLI config</title>
|
||||
</Head>
|
||||
<div className="grid grid-cols-12 relative gap-4">
|
||||
<div className="relative col-span-12 md:col-span-9 transition-all ease-out duration-100">
|
||||
<div className="w-full prose">
|
||||
<h1 className="">CLI configuration</h1>
|
||||
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-800 to-brand-900 my-8"></div>
|
||||
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
|
||||
<div>
|
||||
{TAGS.map((tag) =>
|
||||
specFile.parameters
|
||||
.filter((param: Parameter) => param.tags[0] === tag)
|
||||
.map((parameter: Parameter, index) => (
|
||||
<div>
|
||||
{index === 0 && <h2 className="text-xl capitalize">{tag}</h2>}
|
||||
<div className="mt-8">
|
||||
<div>
|
||||
<Heading tag="h2" parseAnchors={false} customAnchor={parameter.id}>
|
||||
<code>{parameter.title}</code>
|
||||
</Heading>
|
||||
<div className="grid">
|
||||
<div className="border-b pb-8" key={parameter.id}>
|
||||
<div className=" mb-16">
|
||||
<p className="mb-4 scroll-mt-16 mt-0 text-scale-1100 text-base">
|
||||
<ReactMarkdown>{parameter.description}</ReactMarkdown>
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2">
|
||||
Required: <code>{parameter.required.toString()}</code>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
Default:
|
||||
<code>
|
||||
{parameter.default ? parameter.default.toString() : 'None'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{parameter.links &&
|
||||
parameter.links.map((link) => (
|
||||
<div>
|
||||
<h3>See also:</h3>
|
||||
<li>
|
||||
<a href={link.link}>{link.name}</a>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{parameter.links &&
|
||||
parameter.links.map((link) => (
|
||||
<div>
|
||||
<h3>See also:</h3>
|
||||
<li>
|
||||
<a href={link.link}>{link.name}</a>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-3">
|
||||
<div className="sticky top-20 border-l">
|
||||
<span className="block font-mono text-xs uppercase text-scale-1200 pl-5 mb-4">
|
||||
On this page
|
||||
</span>
|
||||
<GuidesTableOfContents list={tocList} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-3">
|
||||
<div className="sticky top-20 border-l">
|
||||
<span className="block font-mono text-xs uppercase text-scale-1200 pl-5 mb-4">
|
||||
On this page
|
||||
</span>
|
||||
<GuidesTableOfContents list={tocList} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { CodeBlock, Tabs } from 'ui'
|
||||
import specFile from '~/../../spec/transforms/storage_v0_openapi_deparsed.json' assert { type: 'json' }
|
||||
import { gen_v3, enrichedOperation } from '~/lib/refGenerator/helpers'
|
||||
import { Tabs } from '~/../../packages/ui'
|
||||
import { gen_v3 } from '~/lib/refGenerator/helpers'
|
||||
|
||||
// @ts-ignore
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
import RefSubLayout from '~/layouts/ref/RefSubLayout'
|
||||
|
||||
export type AcceptedValue = {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 933 KiB |
+401
-299
@@ -18,6 +18,24 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/handbook/contributing</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/handbook/introduction</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/cli</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -78,24 +96,42 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/handbook/contributing</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/handbook/introduction</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/index</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-deep-dive-jwts</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-google-oauth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-gotrue</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-policies</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-captcha</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -133,13 +169,13 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/managing-user-data</loc>
|
||||
<loc>https://supabase.com/docs/guides/auth/enterprise-sso</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/overview</loc>
|
||||
<loc>https://supabase.com/docs/guides/auth/managing-user-data</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -180,6 +216,84 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/cicd-workflow</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/connect-to-postgres</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/cors</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/debugging</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/global-deployments</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/import-maps</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/local-development</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/quickstart</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/schedule-functions</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/secrets</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/storage-caching</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/typescript-support</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/api</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -450,60 +564,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/cicd-workflow</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/cors</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/debugging</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/import-maps</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/local-development</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/quickstart</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/schedule-functions</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/secrets</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/access-control</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -582,6 +642,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/spend-cap</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/ssl-enforcement</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -601,19 +667,25 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/broadcast</loc>
|
||||
<loc>https://supabase.com/docs/guides/realtime/architecture</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/postgres-changes</loc>
|
||||
<loc>https://supabase.com/docs/guides/realtime/channels</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/presence</loc>
|
||||
<loc>https://supabase.com/docs/guides/realtime/extensions</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/protocol</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -630,6 +702,18 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/realtime-with-nextjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/subscribing-to-database-changes</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/examples</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -678,36 +762,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-deep-dive-jwts</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-google-oauth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-gotrue</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-policies</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/auth-ui</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -738,6 +792,24 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/phone-login/messagebird</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/phone-login/twilio</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/phone-login/vonage</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-apple</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -840,12 +912,102 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/sso/auth-sso-saml</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/cloudflare-turnstile</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/discord-bot</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/github-actions</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/og-image</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/openai</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/rate-limiting</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/screenshots</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/stripe-webhooks</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/telegram-bot</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/upstash-redis</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/api/generating-types</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/postgres/dropping-all-tables-in-schema</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/postgres/first-row-in-group</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/postgres/indexes</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/postgres/which-version-of-postgres</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/flutter</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -888,186 +1050,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-angular</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-expo</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-flutter</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-angular</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-react</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-vue</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-nuxt-3</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-react</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-redwoodjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-solidjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-svelte</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-sveltekit</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-vue-3</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/phone-login/messagebird</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/phone-login/twilio</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/phone-login/vonage</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/cloudflare-turnstile</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/connect-to-postgres</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/discord-bot</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/github-actions</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/og-image</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/openai</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/storage-caching</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/stripe-webhooks</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/telegram-bot</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/upstash-redis</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/azure</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/gsuite</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/okta</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/http</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -1224,6 +1206,126 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/azure</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/gsuite</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/okta</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-angular</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-expo</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-flutter</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-angular</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-react</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-vue</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-nuxt-3</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-react</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-redwoodjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-solidjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-svelte</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-sveltekit</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-vue-3</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/extensions/broadcast</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/extensions/postgres-changes</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/extensions/presence</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/migrating-to-supabase/firebase-auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -1260,24 +1362,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/postgres/dropping-all-tables-in-schema</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/postgres/first-row-in-group</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/postgres/which-version-of-postgres</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/realtime/config</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -3560,6 +3644,24 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-ssl-enforcement</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-ssl-enforcement-update</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-ssl-enforcement-get</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-secrets</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
|
||||
@@ -1,962 +0,0 @@
|
||||
// ----------------
|
||||
// 1. CSS Variables
|
||||
// 2. Dark Mode
|
||||
// 3. Autocomplete
|
||||
// 4. Panel
|
||||
// 5. Sources
|
||||
// 6. Hit Layout
|
||||
// 7. Panel Header
|
||||
// 8. Panel Footer
|
||||
// 9. Detached Mode
|
||||
// 10. Gradients
|
||||
// 11. Utilities
|
||||
// ----------------
|
||||
|
||||
// Note:
|
||||
// This theme reflects the markup structure of autocomplete with SCSS indentation.
|
||||
// We use the SASS `@at-root` function to keep specificity low.
|
||||
|
||||
// ----------------
|
||||
// 1. CSS Variables
|
||||
// ----------------
|
||||
:root {
|
||||
// Input
|
||||
--aa-search-input-height: 38px;
|
||||
--aa-input-icon-size: 20px;
|
||||
|
||||
// Size and spacing
|
||||
--aa-base-unit: 16;
|
||||
--aa-spacing-factor: 1;
|
||||
--aa-spacing: calc(var(--aa-base-unit) * var(--aa-spacing-factor) * 1px);
|
||||
--aa-spacing-half: calc(var(--aa-spacing) / 2);
|
||||
--aa-panel-max-height: 650px;
|
||||
|
||||
// Z-index
|
||||
--aa-base-z-index: 9999;
|
||||
|
||||
// Font
|
||||
--aa-font-size: calc(var(--aa-base-unit) * 1px);
|
||||
--aa-font-family: inherit;
|
||||
--aa-font-weight-medium: 500;
|
||||
--aa-font-weight-semibold: 600;
|
||||
--aa-font-weight-bold: 700;
|
||||
|
||||
// Icons
|
||||
--aa-icon-size: 20px;
|
||||
--aa-icon-stroke-width: 1.6;
|
||||
--aa-icon-color-rgb: 119, 119, 163;
|
||||
--aa-icon-color-alpha: 1;
|
||||
--aa-action-icon-size: 20px;
|
||||
|
||||
// Text colors
|
||||
--aa-text-color-rgb: 38, 38, 39;
|
||||
--aa-text-color-alpha: 1;
|
||||
--aa-primary-color-rgb: 62, 52, 211;
|
||||
--aa-primary-color-alpha: 0.2;
|
||||
--aa-muted-color-rgb: 128, 126, 163;
|
||||
--aa-muted-color-alpha: 0.6;
|
||||
|
||||
// Border colors
|
||||
--aa-panel-border-color-rgb: 128, 126, 163;
|
||||
--aa-panel-border-color-alpha: 0.3;
|
||||
--aa-input-border-color-rgb: 128, 126, 163;
|
||||
--aa-input-border-color-alpha: 0.8;
|
||||
|
||||
// Background colors
|
||||
--aa-background-color-rgb: 255, 255, 255;
|
||||
--aa-background-color-alpha: 1;
|
||||
--aa-input-background-color-rgb: 255, 255, 255;
|
||||
--aa-input-background-color-alpha: 1;
|
||||
--aa-selected-color-rgb: 179, 173, 214;
|
||||
--aa-selected-color-alpha: 0.205;
|
||||
--aa-description-highlight-background-color-rgb: 245, 223, 77;
|
||||
--aa-description-highlight-background-color-alpha: 0.5;
|
||||
|
||||
// Detached mode
|
||||
--aa-detached-media-query: (max-width: 680px);
|
||||
--aa-detached-modal-media-query: (min-width: 680px);
|
||||
--aa-detached-modal-max-width: 680px;
|
||||
--aa-detached-modal-max-height: 500px;
|
||||
--aa-overlay-color-rgb: 115, 114, 129;
|
||||
--aa-overlay-color-alpha: 0.4;
|
||||
|
||||
// Shadows
|
||||
--aa-panel-shadow: 0 0 0 1px rgba(35, 38, 59, 0.1), 0 6px 16px -4px rgba(35, 38, 59, 0.15);
|
||||
|
||||
// Scrollbar
|
||||
--aa-scrollbar-width: 13px;
|
||||
--aa-scrollbar-track-background-color-rgb: 234, 234, 234;
|
||||
--aa-scrollbar-track-background-color-alpha: 1;
|
||||
--aa-scrollbar-thumb-background-color-rgb: var(--aa-background-color-rgb);
|
||||
--aa-scrollbar-thumb-background-color-alpha: 1;
|
||||
|
||||
// Touch screens
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
--aa-spacing-factor: 1.2;
|
||||
--aa-action-icon-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------
|
||||
// 2. Dark Mode
|
||||
// ----------------
|
||||
body {
|
||||
/* stylelint-disable selector-no-qualifying-type, selector-class-pattern */
|
||||
&[data-theme='dark'],
|
||||
&.dark {
|
||||
// Text colors
|
||||
--aa-text-color-rgb: 183, 192, 199;
|
||||
--aa-primary-color-rgb: 146, 138, 255;
|
||||
--aa-muted-color-rgb: 146, 138, 255;
|
||||
|
||||
// Background colors
|
||||
--aa-input-background-color-rgb: 0, 3, 9;
|
||||
--aa-background-color-rgb: 21, 24, 42;
|
||||
--aa-selected-color-rgb: 146, 138, 255;
|
||||
--aa-selected-color-alpha: 0.25;
|
||||
--aa-description-highlight-background-color-rgb: 0 255 255;
|
||||
--aa-description-highlight-background-color-alpha: 0.25;
|
||||
|
||||
// Icons
|
||||
--aa-icon-color-rgb: 119, 119, 163;
|
||||
|
||||
// Shadows
|
||||
--aa-panel-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9);
|
||||
|
||||
// Scrollbar
|
||||
--aa-scrollbar-track-background-color-rgb: 44, 46, 64;
|
||||
--aa-scrollbar-thumb-background-color-rgb: var(--aa-background-color-rgb);
|
||||
}
|
||||
/* stylelint-enable selector-no-qualifying-type, selector-class-pattern */
|
||||
}
|
||||
|
||||
// Reset for `@extend`
|
||||
%reset {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// Init for `@extend`
|
||||
%init {
|
||||
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
|
||||
font-family: var(--aa-font-family);
|
||||
font-size: var(--aa-font-size);
|
||||
font-weight: normal;
|
||||
line-height: 1em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
// ----------------
|
||||
// 3. Autocomplete
|
||||
// ----------------
|
||||
.aa-Autocomplete,
|
||||
.aa-DetachedFormContainer {
|
||||
@extend %init;
|
||||
* {
|
||||
@extend %reset;
|
||||
}
|
||||
// Search box
|
||||
@at-root .aa-Form {
|
||||
@apply transition bg-scale-200 border border-scale-500;
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
line-height: 1em;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
&:focus-within {
|
||||
@apply border-brand-900;
|
||||
outline: currentColor none medium;
|
||||
}
|
||||
@at-root .aa-InputWrapperPrefix {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: var(--aa-search-input-height);
|
||||
order: 1;
|
||||
// Container for search and loading icons
|
||||
@at-root .aa-Label,
|
||||
.aa-LoadingIndicator {
|
||||
cursor: initial;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
svg {
|
||||
@apply text-brand-900;
|
||||
height: auto;
|
||||
max-height: var(--aa-input-icon-size);
|
||||
stroke-width: var(--aa-icon-stroke-width);
|
||||
width: var(--aa-input-icon-size);
|
||||
}
|
||||
}
|
||||
@at-root .aa-SubmitButton,
|
||||
.aa-LoadingIndicator {
|
||||
height: 100%;
|
||||
padding-left: calc(var(--aa-spacing) * 0.75 - 1px);
|
||||
padding-right: var(--aa-spacing-half);
|
||||
width: calc(var(--aa-spacing) * 1.75 + var(--aa-icon-size) - 1px);
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
padding-left: calc(var(--aa-spacing-half) / 2 - 1px);
|
||||
width: calc(var(--aa-icon-size) + (var(--aa-spacing) * 1.25) - 1px);
|
||||
}
|
||||
}
|
||||
@at-root .aa-SubmitButton {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
@at-root .aa-LoadingIndicator {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@at-root .aa-InputWrapper {
|
||||
order: 3;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
// Search box input (with placeholder and query)
|
||||
@at-root .aa-Input {
|
||||
@apply text-sm text-scale-1200;
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: 0;
|
||||
height: var(--aa-search-input-height);
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
&::placeholder {
|
||||
@apply text-scale-700;
|
||||
opacity: 1;
|
||||
}
|
||||
// Focus is set and styled on the parent, it isn't necessary here
|
||||
&:focus {
|
||||
border-color: none;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
// Remove native appearence
|
||||
&::-webkit-search-decoration,
|
||||
&::-webkit-search-cancel-button,
|
||||
&::-webkit-search-results-button,
|
||||
&::-webkit-search-results-decoration {
|
||||
appearance: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@at-root .aa-InputWrapperSuffix {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: var(--aa-search-input-height);
|
||||
order: 4;
|
||||
// Accelerator to clear the query
|
||||
@at-root .aa-ClearButton {
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: 0;
|
||||
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0 calc(var(--aa-spacing) * (5 / 6) - 0.5px);
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
padding: 0 calc(var(--aa-spacing) * (2 / 3) - 0.5px);
|
||||
}
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
|
||||
}
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
svg {
|
||||
stroke-width: var(--aa-icon-stroke-width);
|
||||
width: var(--aa-icon-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------
|
||||
// 4. Panel
|
||||
// ----------------
|
||||
.aa-Panel {
|
||||
@extend %init;
|
||||
|
||||
@apply bg-scale-200;
|
||||
border-radius: calc(var(--aa-spacing) / 4);
|
||||
box-shadow: var(--aa-panel-shadow);
|
||||
margin: 8px 0 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
transition: opacity 200ms ease-in, filter 200ms ease-in;
|
||||
@media screen and (prefers-reduced-motion) {
|
||||
transition: none;
|
||||
}
|
||||
* {
|
||||
@extend %reset;
|
||||
}
|
||||
button {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@at-root .aa-PanelLayout {
|
||||
@apply bg-scale-200;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
max-height: var(--aa-panel-max-height);
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
|
||||
@at-root .aa-PanelLayoutColumns--twoGolden {
|
||||
display: grid;
|
||||
grid-template-columns: 39.2% auto;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
@at-root .aa-PanelLayoutColumns--two {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
@at-root .aa-PanelLayoutColumns--three {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// When a request isn't resolved yet
|
||||
@at-root .aa-Panel--stalled {
|
||||
.aa-Source {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@at-root .aa-Panel--scrollable {
|
||||
margin: 0;
|
||||
max-height: var(--aa-panel-max-height);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: var(--aa-spacing-half);
|
||||
scrollbar-color: rgba(
|
||||
var(--aa-scrollbar-thumb-background-color-rgb),
|
||||
var(--aa-scrollbar-thumb-background-color-alpha)
|
||||
)
|
||||
rgba(
|
||||
var(--aa-scrollbar-track-background-color-rgb),
|
||||
var(--aa-scrollbar-track-background-color-alpha)
|
||||
);
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: var(--aa-scrollbar-width);
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: rgba(
|
||||
var(--aa-scrollbar-track-background-color-rgb),
|
||||
var(--aa-scrollbar-track-background-color-alpha)
|
||||
);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(
|
||||
var(--aa-scrollbar-thumb-background-color-rgb),
|
||||
var(--aa-scrollbar-thumb-background-color-alpha)
|
||||
);
|
||||
border-color: rgba(
|
||||
var(--aa-scrollbar-track-background-color-rgb),
|
||||
var(--aa-scrollbar-track-background-color-alpha)
|
||||
);
|
||||
border-radius: 9999px;
|
||||
border-style: solid;
|
||||
border-width: 3px 2px 3px 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------
|
||||
// 5. Sources
|
||||
// Each source can be styled independently
|
||||
// ----------------
|
||||
.aa-Source {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
&:empty {
|
||||
// Hide empty section
|
||||
display: none;
|
||||
}
|
||||
@at-root .aa-SourceNoResults {
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
padding: var(--aa-spacing);
|
||||
}
|
||||
// List of results inside the source
|
||||
@at-root .aa-List {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
// Source title
|
||||
@at-root .aa-SourceHeader {
|
||||
margin: var(--aa-spacing-half) 0.5em var(--aa-spacing-half) 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
// Hide empty header
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
// Title typography
|
||||
@at-root .aa-SourceHeaderTitle {
|
||||
background: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
|
||||
color: rgba(var(--aa-primary-color-rgb), 1);
|
||||
display: inline-block;
|
||||
font-size: 0.8em;
|
||||
font-weight: var(--aa-font-weight-semibold);
|
||||
margin: 0;
|
||||
padding: 0 var(--aa-spacing-half) 0 0;
|
||||
position: relative;
|
||||
z-index: var(--aa-base-z-index);
|
||||
}
|
||||
// Line separator
|
||||
@at-root .aa-SourceHeaderLine {
|
||||
border-bottom: solid 1px rgba(var(--aa-primary-color-rgb), 1);
|
||||
display: block;
|
||||
height: 2px;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
opacity: 0.3;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: var(--aa-spacing-half);
|
||||
z-index: calc(var(--aa-base-z-index) - 1);
|
||||
}
|
||||
}
|
||||
// See all button
|
||||
@at-root .aa-SourceFooterSeeAll {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha)),
|
||||
rgba(128, 126, 163, 0.14)
|
||||
);
|
||||
border: 1px solid rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
|
||||
border-radius: 5px;
|
||||
box-shadow: inset 0 0 2px #fff, 0 2px 2px -1px rgba(76, 69, 88, 0.15);
|
||||
color: inherit;
|
||||
font-size: 0.95em;
|
||||
font-weight: var(--aa-font-weight-medium);
|
||||
padding: 0.475em 1em 0.6em;
|
||||
text-decoration: none;
|
||||
&:focus,
|
||||
&:hover {
|
||||
border: 1px solid rgba(var(--aa-primary-color-rgb), 1);
|
||||
color: rgba(var(--aa-primary-color-rgb), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------
|
||||
// 6. Hit Layout
|
||||
// ----------------
|
||||
.aa-Item {
|
||||
@apply px-4 py-4;
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
min-height: calc(var(--aa-spacing) * 2.5);
|
||||
// When the result is active
|
||||
&[aria-selected='true'] {
|
||||
@apply bg-scale-400;
|
||||
.aa-ItemActionButton,
|
||||
.aa-ActiveOnly {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
// The result type icon inlined SVG or image
|
||||
@at-root .aa-ItemIcon {
|
||||
align-items: center;
|
||||
background: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 0 1px
|
||||
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
|
||||
color: rgba(var(--aa-icon-color-rgb), var(--aa-icon-color-alpha));
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.7em;
|
||||
height: calc(var(--aa-icon-size) + var(--aa-spacing-half));
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
stroke-width: var(--aa-icon-stroke-width);
|
||||
text-align: center;
|
||||
width: calc(var(--aa-icon-size) + var(--aa-spacing-half));
|
||||
img {
|
||||
height: auto;
|
||||
max-height: calc(var(--aa-icon-size) + var(--aa-spacing-half) - 8px);
|
||||
max-width: calc(var(--aa-icon-size) + var(--aa-spacing-half) - 8px);
|
||||
width: auto;
|
||||
}
|
||||
svg {
|
||||
height: var(--aa-icon-size);
|
||||
width: var(--aa-icon-size);
|
||||
}
|
||||
@at-root .aa-ItemIcon--alignTop {
|
||||
align-self: flex-start;
|
||||
}
|
||||
@at-root .aa-ItemIcon--noBorder {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
@at-root .aa-ItemIcon--picture {
|
||||
height: 96px;
|
||||
width: 96px;
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
padding: var(--aa-spacing-half);
|
||||
}
|
||||
}
|
||||
}
|
||||
@at-root .aa-ItemContent {
|
||||
@apply flex flex-col space-y-1;
|
||||
cursor: pointer;
|
||||
line-height: 1.25em;
|
||||
overflow: hidden;
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
mark {
|
||||
@apply text-brand-900;
|
||||
background: none;
|
||||
font-style: normal;
|
||||
font-weight: var(--aa-font-weight-bold);
|
||||
}
|
||||
.aa-ItemTitle {
|
||||
@apply text-scale-1200 text-sm;
|
||||
}
|
||||
@at-root .aa-ItemContent--dual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
text-align: left;
|
||||
.aa-ItemContentTitle,
|
||||
.aa-ItemContentSubtitle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@at-root .aa-ItemContent--indented {
|
||||
padding-left: calc(var(--aa-icon-size) + var(--aa-spacing));
|
||||
}
|
||||
@at-root .aa-ItemContentBody {
|
||||
display: grid;
|
||||
gap: calc(var(--aa-spacing-half) / 2);
|
||||
}
|
||||
@at-root .aa-ItemContentTitle {
|
||||
display: inline-block;
|
||||
margin: 0 0.5em 0 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@at-root .aa-ItemContentSubtitle {
|
||||
@apply text-scale-1000 text-sm truncate;
|
||||
@at-root .aa-ItemContentSubtitleIcon {
|
||||
&::before {
|
||||
border-color: rgba(var(--aa-muted-color-rgb), 0.64);
|
||||
border-style: solid;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
left: 1px;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
}
|
||||
}
|
||||
@at-root .aa-ItemContentSubtitle--inline {
|
||||
.aa-ItemContentSubtitleIcon {
|
||||
&::before {
|
||||
border-width: 0 0 1.5px;
|
||||
margin-left: var(--aa-spacing-half);
|
||||
margin-right: calc(var(--aa-spacing-half) / 2);
|
||||
width: calc(var(--aa-spacing-half) + 2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
@at-root .aa-ItemContentSubtitle--standalone {
|
||||
align-items: center;
|
||||
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
|
||||
display: grid;
|
||||
gap: var(--aa-spacing-half);
|
||||
grid-auto-flow: column;
|
||||
justify-content: start;
|
||||
.aa-ItemContentSubtitleIcon {
|
||||
&::before {
|
||||
border-radius: 0 0 0 3px;
|
||||
border-width: 0 0 1.5px 1.5px;
|
||||
height: var(--aa-spacing-half);
|
||||
width: var(--aa-spacing-half);
|
||||
}
|
||||
}
|
||||
}
|
||||
@at-root .aa-ItemContentSubtitleCategory {
|
||||
color: rgba(var(--aa-muted-color-rgb), 1);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@at-root .aa-ItemContentDescription {
|
||||
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
|
||||
font-size: 0.85em;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
mark {
|
||||
background: rgba(
|
||||
var(--aa-description-highlight-background-color-rgb),
|
||||
var(--aa-description-highlight-background-color-alpha)
|
||||
);
|
||||
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
|
||||
font-style: normal;
|
||||
font-weight: var(--aa-font-weight-medium);
|
||||
}
|
||||
}
|
||||
@at-root .aa-ItemContentDash {
|
||||
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
|
||||
display: none;
|
||||
opacity: 0.4;
|
||||
}
|
||||
@at-root .aa-ItemContentTag {
|
||||
background-color: rgba(var(--aa-primary-color-rgb), var(--aa-primary-color-alpha));
|
||||
border-radius: 3px;
|
||||
margin: 0 0.4em 0 0;
|
||||
padding: 0.08em 0.3em;
|
||||
}
|
||||
}
|
||||
// wrap hit with url but we don't need to see it
|
||||
@at-root .aa-ItemWrapper,
|
||||
.aa-ItemLink {
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
gap: calc(var(--aa-spacing-half) / 2);
|
||||
grid-auto-flow: column;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
@at-root .aa-ItemLink {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
// Secondary click actions
|
||||
@at-root .aa-ItemActions {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
height: 100%;
|
||||
justify-self: end;
|
||||
margin: 0 calc(var(--aa-spacing) / -3);
|
||||
padding: 0 2px 0 0;
|
||||
}
|
||||
@at-root .aa-ItemActionButton {
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: 0;
|
||||
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
&:hover,
|
||||
&:focus {
|
||||
svg {
|
||||
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
svg {
|
||||
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
|
||||
margin: 0;
|
||||
margin: calc(var(--aa-spacing) / 3);
|
||||
stroke-width: var(--aa-icon-stroke-width);
|
||||
width: var(--aa-action-icon-size);
|
||||
}
|
||||
}
|
||||
@at-root .aa-ActiveOnly {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
//----------------
|
||||
// 7. Panel Header
|
||||
//----------------
|
||||
.aa-PanelHeader {
|
||||
align-items: center;
|
||||
background: rgba(var(--aa-primary-color-rgb), 1);
|
||||
color: #fff;
|
||||
display: grid;
|
||||
height: var(--aa-modal-header-height);
|
||||
margin: 0;
|
||||
padding: var(--aa-spacing-half) var(--aa-spacing);
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
background-image: linear-gradient(
|
||||
rgba(var(--aa-background-color-rgb), 1),
|
||||
rgba(var(--aa-background-color-rgb), 0)
|
||||
);
|
||||
bottom: calc(var(--aa-spacing-half) * -1);
|
||||
content: '';
|
||||
height: var(--aa-spacing-half);
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: var(--aa-base-z-index);
|
||||
}
|
||||
}
|
||||
|
||||
//----------------
|
||||
// 8. Panel Footer
|
||||
//----------------
|
||||
.aa-PanelFooter {
|
||||
background-color: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
|
||||
box-shadow: inset 0 1px 0
|
||||
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
padding: var(--aa-spacing);
|
||||
position: relative;
|
||||
z-index: var(--aa-base-z-index);
|
||||
&::after {
|
||||
background-image: linear-gradient(
|
||||
rgba(var(--aa-background-color-rgb), 0),
|
||||
rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha))
|
||||
);
|
||||
content: '';
|
||||
height: var(--aa-spacing);
|
||||
left: 0;
|
||||
opacity: 0.12;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(var(--aa-spacing) * -1);
|
||||
z-index: calc(var(--aa-base-z-index) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
//----------------
|
||||
// 9. Detached Mode
|
||||
//----------------
|
||||
.aa-DetachedContainer {
|
||||
@apply bg-scale-200 border border-scale-500;
|
||||
bottom: 0;
|
||||
box-shadow: var(--aa-panel-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: var(--aa-base-z-index);
|
||||
&::after {
|
||||
height: 32px;
|
||||
}
|
||||
.aa-SourceHeader {
|
||||
margin: var(--aa-spacing-half) 0 var(--aa-spacing-half) 2px;
|
||||
}
|
||||
.aa-Panel {
|
||||
@apply bg-scale-200;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
.aa-PanelLayout {
|
||||
bottom: 0;
|
||||
box-shadow: none;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
max-height: none;
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@at-root .aa-DetachedFormContainer {
|
||||
@apply bg-scale-200;
|
||||
border-bottom: solid 1px
|
||||
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
padding: var(--aa-spacing-half);
|
||||
@at-root .aa-DetachedCancelButton {
|
||||
@apply text-scale-1100 text-sm;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
margin: 0 0 0 var(--aa-spacing-half);
|
||||
padding: 0 var(--aa-spacing-half);
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 1px
|
||||
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
@at-root .aa-DetachedContainer--modal {
|
||||
border-radius: 6px;
|
||||
bottom: inherit;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
max-width: var(--aa-detached-modal-max-width);
|
||||
position: absolute;
|
||||
top: 3%;
|
||||
.aa-PanelLayout {
|
||||
max-height: var(--aa-detached-modal-max-height);
|
||||
padding-bottom: var(--aa-spacing-half);
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Search Button
|
||||
.aa-DetachedSearchButton {
|
||||
@apply rounded-md bg-scaleA-200 border border-scale-700;
|
||||
align-items: center;
|
||||
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font: inherit;
|
||||
font-family: var(--aa-font-family);
|
||||
font-size: var(--aa-font-size);
|
||||
height: var(--aa-search-input-height);
|
||||
margin: 0;
|
||||
padding: 0 calc(var(--aa-search-input-height) / 8);
|
||||
position: relative;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
&:focus {
|
||||
@apply border-brand-900;
|
||||
outline: currentColor none medium;
|
||||
}
|
||||
@at-root .aa-DetachedSearchButtonIcon {
|
||||
@apply text-scale-1100;
|
||||
align-items: center;
|
||||
cursor: initial;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
width: calc(var(--aa-icon-size) + var(--aa-spacing));
|
||||
}
|
||||
.aa-DetachedSearchButtonPlaceholder {
|
||||
@apply text-sm text-scale-800;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove scroll on `body`
|
||||
.aa-Detached {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aa-DetachedOverlay {
|
||||
@apply backdrop-blur-sm;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: calc(var(--aa-base-z-index) - 1);
|
||||
}
|
||||
|
||||
//----------------
|
||||
// 10. Gradients
|
||||
//----------------
|
||||
.aa-GradientTop,
|
||||
.aa-GradientBottom {
|
||||
height: var(--aa-spacing-half);
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: var(--aa-base-z-index);
|
||||
}
|
||||
|
||||
.aa-GradientTop {
|
||||
// background-image: linear-gradient(
|
||||
// rgba(var(--aa-background-color-rgb), 1),
|
||||
// rgba(var(--aa-background-color-rgb), 0)
|
||||
// );
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.aa-GradientBottom {
|
||||
// background-image: linear-gradient(
|
||||
// rgba(var(--aa-background-color-rgb), 0),
|
||||
// rgba(var(--aa-background-color-rgb), 1)
|
||||
// );
|
||||
// border-bottom-left-radius: calc(var(--aa-spacing) / 4);
|
||||
// border-bottom-right-radius: calc(var(--aa-spacing) / 4);
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
//----------------
|
||||
// 11. Utilities
|
||||
//----------------
|
||||
.aa-DesktopOnly {
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.aa-TouchOnly {
|
||||
@media (hover: hover) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// search bar
|
||||
input[type='search'].DocSearch-Input {
|
||||
background-color: var(--colors-scale1);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark .DocSearch-Input {
|
||||
background-color: var(--colors-scale2);
|
||||
color: white;
|
||||
}
|
||||
@@ -1,535 +0,0 @@
|
||||
/* search modal */
|
||||
|
||||
.DocSearch--active {
|
||||
@apply overflow-hidden #{!important};
|
||||
}
|
||||
|
||||
.DocSearch-Container {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
@apply fixed top-0 left-0 z-[200] flex h-screen w-screen flex-col p-4 backdrop-blur-sm sm:p-6 md:p-[10vh] lg:p-[12vh];
|
||||
@apply cursor-auto #{!important};
|
||||
}
|
||||
|
||||
.DocSearch-LoadingIndicator svg {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.DocSearch-LoadingIndicator {
|
||||
display: none;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'%3E%3Ccircle cx='12' cy='12' r='9' stroke-width='2' stroke='%23cffafe' /%3E%3Cpath d='M3,12a9,9 0 1,0 18,0a9,9 0 1,0 -18,0' stroke-width='2' stroke='%2306b6d4' stroke-dasharray='56.5486677646' stroke-dashoffset='37.6991118431' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.DocSearch-Container--Stalled .DocSearch-LoadingIndicator {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.DocSearch-Modal {
|
||||
@apply dark:bg-scale-200 bg-scale-100;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 47.375rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
border-radius: theme('borderRadius.lg');
|
||||
box-shadow: theme('boxShadow.lg');
|
||||
}
|
||||
|
||||
.DocSearch-SearchBar {
|
||||
z-index: 1;
|
||||
padding: 0 1rem;
|
||||
@apply border-scale-400 relative flex flex-none items-center border-b;
|
||||
}
|
||||
|
||||
.DocSearch-Form {
|
||||
@apply flex min-w-0 flex-auto items-center;
|
||||
}
|
||||
|
||||
.DocSearch-Dropdown-Container {
|
||||
@apply pb-6;
|
||||
}
|
||||
|
||||
.DocSearch-Dropdown {
|
||||
@apply flex-auto overflow-auto;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Result {
|
||||
@apply relative mx-6;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Result.DocSearch-Hit--Child {
|
||||
@apply ml-12;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Result.DocSearch-Hit--Child::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -0.25rem;
|
||||
bottom: -0.25rem;
|
||||
left: -1rem;
|
||||
width: 1px;
|
||||
background: theme('colors.scale.400');
|
||||
|
||||
// @apply dark:bg-slate-700;
|
||||
}
|
||||
|
||||
.DocSearch-Hit > a {
|
||||
position: relative;
|
||||
font-size: 0.875rem;
|
||||
// border-bottom: 1px solid theme('colors.slate.100');
|
||||
|
||||
// @apply dark:border-slate-200;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Result {
|
||||
// @apply border-b-0 #{!important};
|
||||
}
|
||||
|
||||
.DocSearch-MagnifierLabel {
|
||||
@apply h-6 w-6 flex-none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m19 19-3.5-3.5' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='11' cy='11' r='6' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
|
||||
.dark & {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m19 19-3.5-3.5' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='11' cy='11' r='6' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-MagnifierLabel svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Container--Stalled .DocSearch-MagnifierLabel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Input {
|
||||
@apply text-scale-1200 placeholder-scale-900;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
height: 3.5rem;
|
||||
font-size: 1rem;
|
||||
margin-left: 0.75rem;
|
||||
margin-right: 1rem;
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@screen sm {
|
||||
.DocSearch-Input {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Input:focus {
|
||||
outline: 2px dotted transparent;
|
||||
}
|
||||
|
||||
.DocSearch-Input::-webkit-search-cancel-button,
|
||||
.DocSearch-Input::-webkit-search-decoration,
|
||||
.DocSearch-Input::-webkit-search-results-button,
|
||||
.DocSearch-Input::-webkit-search-results-decoration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Reset {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button[type='reset'].DocSearch-Cancel {
|
||||
appearance: none;
|
||||
flex: none;
|
||||
font-size: 0;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.25rem 0.375rem;
|
||||
@apply ring-scale-900 hover:ring-scale-900 dark:bg-scale-600 shadow-sm ring-1 hover:shadow dark:ring-0;
|
||||
width: 1.75rem;
|
||||
height: 1.5rem;
|
||||
/* esc */
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='16' height='7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M.506 6h3.931V4.986H1.736v-1.39h2.488V2.583H1.736V1.196h2.69V.182H.506V6ZM8.56 1.855h1.18C9.721.818 8.87.102 7.574.102c-1.276 0-2.21.705-2.205 1.762-.003.858.602 1.35 1.585 1.585l.634.159c.633.153.986.335.988.727-.002.426-.406.716-1.03.716-.64 0-1.1-.295-1.14-.878h-1.19c.03 1.259.931 1.91 2.343 1.91 1.42 0 2.256-.68 2.259-1.745-.003-.969-.733-1.483-1.744-1.71l-.523-.125c-.506-.117-.93-.304-.92-.722 0-.375.332-.65.934-.65.588 0 .949.267.994.724ZM15.78 2.219C15.618.875 14.6.102 13.254.102c-1.537 0-2.71 1.086-2.71 2.989 0 1.898 1.153 2.989 2.71 2.989 1.492 0 2.392-.992 2.526-2.063l-1.244-.006c-.117.623-.606.98-1.262.98-.883 0-1.483-.656-1.483-1.9 0-1.21.591-1.9 1.492-1.9.673 0 1.159.389 1.253 1.028h1.244Z' fill='%23334155'/%3E%3C/svg%3E");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 57.1428571429% auto;
|
||||
|
||||
.dark & {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='16' height='7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M.506 6h3.931V4.986H1.736v-1.39h2.488V2.583H1.736V1.196h2.69V.182H.506V6ZM8.56 1.855h1.18C9.721.818 8.87.102 7.574.102c-1.276 0-2.21.705-2.205 1.762-.003.858.602 1.35 1.585 1.585l.634.159c.633.153.986.335.988.727-.002.426-.406.716-1.03.716-.64 0-1.1-.295-1.14-.878h-1.19c.03 1.259.931 1.91 2.343 1.91 1.42 0 2.256-.68 2.259-1.745-.003-.969-.733-1.483-1.744-1.71l-.523-.125c-.506-.117-.93-.304-.92-.722 0-.375.332-.65.934-.65.588 0 .949.267.994.724ZM15.78 2.219C15.618.875 14.6.102 13.254.102c-1.537 0-2.71 1.086-2.71 2.989 0 1.898 1.153 2.989 2.71 2.989 1.492 0 2.392-.992 2.526-2.063l-1.244-.006c-.117.623-.606.98-1.262.98-.883 0-1.483-.656-1.483-1.9 0-1.21.591-1.9 1.492-1.9.673 0 1.159.389 1.253 1.028h1.244Z' fill='%2394a3b8'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Reset svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-source {
|
||||
line-height: 1.5rem;
|
||||
font-weight: 400;
|
||||
color: theme('colors.scale.900');
|
||||
padding-top: 2.5rem;
|
||||
margin: 0 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.DocSearch-Hits:first-child .DocSearch-Hit-source {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-Container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-Tree {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Result .DocSearch-Hit-icon {
|
||||
display: block;
|
||||
flex: none;
|
||||
margin-right: 1rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: theme('borderRadius.md');
|
||||
/* hash icon */
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.75 1v10M8.25 1v10M1 3.75h10M1 8.25h10' stroke='%2394A3B8' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
|
||||
@apply ring-scale-300 bg-white shadow-sm ring-1 dark:bg-slate-700 dark:ring-0;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Parent.DocSearch-Hit--Guide .DocSearch-Hit-icon {
|
||||
/* book icon */
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='14' height='10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M3.5 0c1.093 0 2.117.27 3 .743V10a6.344 6.344 0 0 0-3-.743c-1.093 0-2.617.27-3.5.743V.743C.883.27 2.407 0 3.5 0Z' fill='%23BAE6FD'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.5 0c1.093 0 2.617.27 3.5.743V10c-.883-.473-2.407-.743-3.5-.743s-2.117.27-3 .743V.743a6.344 6.344 0 0 1 3-.743Z' fill='%2338BDF8'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.DocSearch-Hit-icon svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-content-wrapper {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
min-width: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-path {
|
||||
align-self: flex-start;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5rem;
|
||||
font-weight: 400;
|
||||
color: theme('colors.scale.1100');
|
||||
border-radius: 999px;
|
||||
padding: 0 0.75rem;
|
||||
@apply bg-scale-300 dark:bg-scale-500;
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-icon {
|
||||
// @apply bg-transparent shadow-none ring-1 ring-white/30 dark:ring-white/20;
|
||||
// background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.75 1v10M8.25 1v10M1 3.75h10M1 8.25h10' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit--Parent .DocSearch-Hit-icon {
|
||||
/* book icon */
|
||||
// background-image: url("data:image/svg+xml,%3Csvg width='14' height='10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M3.5 0c1.093 0 2.117.27 3 .743V10a6.344 6.344 0 0 0-3-.743c-1.093 0-2.617.27-3.5.743V.743C.883.27 2.407 0 3.5 0Z' fill='%23fff'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.5 0c1.093 0 2.617.27 3.5.743V10c-.883-.473-2.407-.743-3.5-.743s-2.117.27-3 .743V.743a6.344 6.344 0 0 1 3-.743Z' fill='%23fff'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.DocSearch-Hit-title {
|
||||
@apply text-scale-1100 dark:text-scale-1100 truncate leading-6;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-title + .DocSearch-Hit-path {
|
||||
@apply mb-1;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action {
|
||||
@apply ml-3.5 flex-none;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Result {
|
||||
.DocSearch-Hit-action svg {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action {
|
||||
// @apply h-6 w-6;
|
||||
// background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m11 9 3 3-3 3' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
[aria-selected='true'] & .DocSearch-Hit-action {
|
||||
// background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m11 9 3 3-3 3' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected='true'] & .DocSearch-Hit-path {
|
||||
// @apply bg-scale-400 dark:bg-scale-400;
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected='true'] & .DocSearch-Hit-title {
|
||||
// @apply text-white;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action-button {
|
||||
@apply flex;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action + .DocSearch-Hit-action {
|
||||
@apply ml-2;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action path {
|
||||
stroke-width: 2px;
|
||||
stroke: #71717a;
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-action path {
|
||||
stroke: #777;
|
||||
|
||||
.dark & {
|
||||
stroke: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Hit > a {
|
||||
@apply block py-4 px-6;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Result {
|
||||
@apply py-3 px-4 #{!important};
|
||||
}
|
||||
|
||||
.DocSearch-Hit {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.DocSearch-Hit:first-child > a {
|
||||
// @apply border-t border-slate-100 dark:border-slate-200;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Result {
|
||||
// @apply border-t-0 #{!important};
|
||||
}
|
||||
|
||||
.DocSearch-Hit + .DocSearch-Hit .DocSearch-Hit--Result {
|
||||
@apply mt-2;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Result {
|
||||
@apply dark:bg-scale-300 border-scale-400 rounded-lg border bg-white transition-all;
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected='true'] > a {
|
||||
@apply bg-slate-50 dark:bg-slate-700;
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit--Result {
|
||||
@apply bg-scale-200 dark:bg-scale-600;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--FirstChild::before {
|
||||
@apply top-0 #{!important};
|
||||
}
|
||||
|
||||
.DocSearch-Hit--LastChild::before {
|
||||
@apply bottom-0 #{!important};
|
||||
}
|
||||
|
||||
.DocSearch-Hits mark {
|
||||
background: none;
|
||||
@apply text-scale-1200 border-scale-900 border-b-2 font-semibold;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-path mark {
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected='true'] mark {
|
||||
@apply text-scale-1200;
|
||||
border-bottom-color: currentColor;
|
||||
}
|
||||
|
||||
.DocSearch-Footer {
|
||||
@apply flex flex-none justify-end border-t border-slate-100 py-4 px-6 dark:border-slate-200;
|
||||
}
|
||||
|
||||
.DocSearch-Commands {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.DocSearch-Logo a {
|
||||
@apply text-scale-900 flex items-center text-xs font-medium;
|
||||
}
|
||||
|
||||
.DocSearch-Logo svg {
|
||||
@apply text-scale-900 ml-3 text-[#5468ff];
|
||||
}
|
||||
|
||||
.DocSearch-Hit--deleting,
|
||||
.DocSearch-Hit--favoriting {
|
||||
opacity: 0;
|
||||
transition: all 250ms linear;
|
||||
}
|
||||
|
||||
.DocSearch-NoResults .DocSearch-Screen-Icon {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.DocSearch-Title {
|
||||
@apply mb-10 text-lg leading-6;
|
||||
}
|
||||
|
||||
.DocSearch-Title strong {
|
||||
@apply text-slate-1200 dark:text-scale-1200 font-normal;
|
||||
}
|
||||
|
||||
.DocSearch-NoResults-Prefill-List {
|
||||
.DocSearch-Help {
|
||||
color: var(--colors-scale11) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-StartScreen {
|
||||
.DocSearch-Help {
|
||||
@apply text-center py-16;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Help {
|
||||
@apply text-scale-1100;
|
||||
font-size: 0.875rem !important;
|
||||
a {
|
||||
color: var(--colors-brand9) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-NoResults {
|
||||
@apply px-4 pt-10 pb-8;
|
||||
}
|
||||
|
||||
.DocSearch-NoResults .DocSearch-Title {
|
||||
text-align: center;
|
||||
color: theme('colors.scale.1100');
|
||||
}
|
||||
|
||||
.DocSearch-NoResults-Prefill-List .DocSearch-Help {
|
||||
@apply text-scale-900 dark:text-scale-200 mb-3 text-sm leading-6;
|
||||
}
|
||||
|
||||
.DocSearch-NoResults-Prefill-List ul {
|
||||
@apply border-scale-300 dark:border-scale-400 dark:bg-scale-300 overflow-hidden rounded-lg border bg-white;
|
||||
}
|
||||
|
||||
.DocSearch-NoResults-Prefill-List button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: theme('fontSize.sm');
|
||||
line-height: theme('lineHeight.6');
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m11 9 3 3-3 3' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.75rem center;
|
||||
|
||||
@apply text-scale-900 hover:text-slate-1200 dark:bg-scale-300 hover:bg-scale-200 dark:hover:bg-scale-400 bg-white;
|
||||
}
|
||||
|
||||
.DocSearch-NoResults-Prefill-List li + li button {
|
||||
@apply border-scale-300 border-t dark:border-slate-400;
|
||||
}
|
||||
|
||||
.DocSearch-NoResults-Prefill-List + .DocSearch-Help {
|
||||
@apply mt-3 text-xs leading-6;
|
||||
}
|
||||
|
||||
.DocSearch-NoResults-Prefill-List + .DocSearch-Help a {
|
||||
@apply text-scale-900 dark:text-scale-900 font-semibold;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action [data-cy] {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action [data-cy] svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action [data-cy='fav-recent'] {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' stroke='%23CBD5E1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
|
||||
.dark & {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action [data-cy='fav-recent']:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' stroke='%230ea5e9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action [data-cy='remove-recent'] {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
|
||||
.dark & {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action [data-cy='remove-recent']:hover {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
|
||||
.dark & {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action [data-cy='remove-fav']::before {
|
||||
@apply pointer-events-none absolute inset-0 bg-sky-50 content-[''] dark:bg-slate-700;
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected='true'] [data-cy='remove-fav']::before {
|
||||
@apply bg-sky-100 dark:bg-slate-700;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action [data-cy='remove-fav']::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
width: calc(4.5rem + 1px);
|
||||
height: 1.5rem;
|
||||
margin-left: calc(-3rem - 1px);
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
|
||||
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
|
||||
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat, repeat-y, no-repeat;
|
||||
background-position: left, center, right;
|
||||
|
||||
.dark & {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
|
||||
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' opacity='0.05' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
|
||||
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action [data-cy='remove-fav']:hover::after {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
|
||||
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
|
||||
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
|
||||
.dark & {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
|
||||
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' opacity='0.05' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
|
||||
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action + .DocSearch-Hit-action {
|
||||
// @apply ml-3 border-l border-slate-200 pl-3 dark:border-slate-200;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
title: Supabase Beta February 2023
|
||||
description: There is something for everybody this month - AI, Auth, Database, Edge Functions, GraphQL … you name it!
|
||||
author: ant_wilson
|
||||
image: 2023-03-09-beta-update-february/supabase-beta-update-february-2023.jpg
|
||||
thumb: 2023-03-09-beta-update-february/supabase-beta-update-february-2023.jpg
|
||||
tags:
|
||||
- release-notes
|
||||
date: '2023-03-09'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
There’s something for everybody this month: AI, Auth, Database, Edge Functions, GraphQL … you name it!
|
||||
|
||||
## GraphiQL editor in the dashboard
|
||||
|
||||

|
||||
|
||||
The most popular GraphQL IDE/API explorer is now built into the dashboard! You can now explore and query your GraphQL API produced by `pg_graphql`.
|
||||
|
||||
[Try it now](https://app.supabase.com/project/_/api/graphiql).
|
||||
|
||||
## Supabase + OpenAI search
|
||||
|
||||

|
||||
|
||||
We've updated our Docs search functionality to use `pgvector` + OpenAI. Still no cease and desist from Microsoft, so you can continue to ask Clippy any Supabase-specific questions 📎💚
|
||||
|
||||
[Ask Clippy.](https://supabase.com/docs)
|
||||
|
||||
## Serve all the functions!
|
||||
|
||||

|
||||
|
||||
Do you use multiple Edge Functions in your project? Then celebrate! Supabase CLI 1.36+ now supports serving multiple Edge Functions at the same time.
|
||||
|
||||
To enable the feature, just run `supabase functions serve` in your project.
|
||||
|
||||
[Check the docs.](https://supabase.com/docs/reference/cli/supabase-functions-serve)
|
||||
|
||||
## Smaller Postgres docker images for everyone
|
||||
|
||||

|
||||
|
||||
We rewrote the Postgres Dockerfile with multi-stage builds so that each extension is compiled in its own separate stage. This reduces the size of the image from 1.3GB to 250MB, enabling a much faster boot time.
|
||||
|
||||
[See it yourself](https://hub.docker.com/r/supabase/postgres/tags)
|
||||
|
||||
## New UI for Postgres Roles
|
||||
|
||||

|
||||
|
||||
We've improved database role management. You can create, update, and delete database roles through the dashboard. Just one small step towards column-level security
|
||||
|
||||
[Check it out.](https://app.supabase.com/project/_/editor)
|
||||
|
||||
## API docs in the table editor
|
||||
|
||||

|
||||
|
||||
API docs got a light touchup and were moved to the table editor. You can now look up API methods and generate & download type files right there ✨
|
||||
|
||||
[Check it out.](https://app.supabase.com/project/_/editor)
|
||||
|
||||
## Quick product updates
|
||||
|
||||
- **Postgres Extensions**: We're rolling out some fixes for several Postgres extensions. Check your Dashboard notifications to see if you need to take any actions.
|
||||
|
||||
- **Auth**: Added full OpenAPI 3.0 spec which provides a comprehensive overview of the API with documentation on each request. [PR](https://github.com/supabase/gotrue/pull/918)
|
||||
|
||||
- **Database**: supabase-js now infers the response type from your query. If the inferred type is incorrect, you can use `.returns<MyType>()` to override it. [Doc](https://supabase.com/docs/reference/javascript/db-returns)
|
||||
|
||||
- **Dashboard**: Improved database roles management, you can now create, update and delete database roles through the dashboard. [Dashboard](https://app.supabase.com/project/_/database/roles)
|
||||
|
||||
- **Dashboard**: We've provided a reference panel showing all available paths that can be queried from each respective source that improves the Logs Explorer experience. [Dashboard](https://app.supabase.com/project/_/logs/explorer)
|
||||
|
||||
- **Edge Functions**: upgraded to Deno 1.30.3, that supports TypeScript 4.9.x and introduces `satisfies`. Thanks to [Benjamin Dobell](https://github.com/Benjamin-Dobell) 🙏. [PR](https://github.com/supabase/functions-relay/pull/30)
|
||||
|
||||
- Realtime: Broadcast only primary key(s) for deleting records when RLS is enabled and replica identity is full. [PR](https://github.com/supabase/realtime/pull/461)
|
||||
|
||||
## Next.js with Supabase
|
||||
|
||||

|
||||
|
||||
Building the next big thing with Supabase and Next.js? Then make sure to check Jon's new video series to help get you (and your app directory) off the ground.
|
||||
|
||||
- [Fetching and Caching data in Next.js Async Server Components](https://www.youtube.com/watch?v=GniRj1jIhFw)
|
||||
|
||||
- [Client vs Server Components in Next.js app directory // Merging server state with realtime updates](https://www.youtube.com/watch?v=YR-xP6PPXXA)
|
||||
|
||||
- [Delicious Server Component Cookies // Auth in the Next.js app directory](https://www.youtube.com/watch?v=Bh1TOpOcGJQ)
|
||||
|
||||

|
||||
|
||||
[Domain-specific ChatGPT](https://github.com/gannonh/gpt3.5-turbo-pgvector) | This starter app uses embeddings coupled with vector search to show how the OpenAI API can be used to create a conversational interface to domain-specific knowledge. Built by [Gannon Hall](https://github.com/gannonh) with Next.js, Vercel, gpt-3.5-turbo, pgvector, and Supabase.
|
||||
|
||||
Discover 100+ other apps in this [tweet](https://twitter.com/supabase/status/1628843899996209155).
|
||||
|
||||
## Extended Community Highlights
|
||||
|
||||

|
||||
|
||||
- Langchain supports our database as a vector store, using pgvector. [Doc](https://hwchase17.github.io/langchainjs/docs/modules/indexes/vector_stores/supabase/) <br/>
|
||||
|
||||
- refine week: building a CRUD app with refine & Supabase. [Full series](https://refine.dev/week-of-refine/)
|
||||
|
||||
- Build an Instagram Web App with Supabase and Next.js. [Tutorial](https://livecycle.io/blogs/nextjs-supabase-instagram/) <br/>
|
||||
|
||||
- Supabase GeoQueries with PostGIS and Storage Upload. [Video](https://www.youtube.com/watch?v=-_Wpr7rwU8I) <br/>
|
||||
|
||||
- How I used Supabase to build a winning hackathon project. [Article](https://blog.matt.lgbt/how-i-used-supabase-to-build-a-winning-hackathon-project)
|
||||
|
||||
- Using a Supabase API within FlutterFlow. [Video](https://www.youtube.com/watch?v=YzSXwlA3vWM) <br/>
|
||||
|
||||
- Getting started with Supabase in Android. [Part 1](https://www.youtube.com/watch?v=SGr73sWMX6w) | [Part 2](https://www.youtube.com/watch?v=NWaIIRfVpuo)
|
||||
|
||||
- Streaming User Events from PostgreSQL (Supabase) to Serverless Kafka. [Article](https://upstash.com/blog/postgre-supabase-connector)<br/>
|
||||
|
||||
- Building a CRUD API with FastAPI and Supabase: A Step-by-Step Guide. [Tutorial](https://blog.theinfosecguy.xyz/building-a-crud-api-with-fastapi-and-supabase-a-step-by-step-guide)<br/>
|
||||
|
||||
- How to build a collaborative SaaS product using Next.js and ZenStack's access control policy. [Tutorial](https://zenstack.dev/blog/saas-demo)<br/>
|
||||
|
||||
- Nuxt 3, Tailwind, and Supabase Crash Course. [Course](https://dev.to/jacobandrewsky/nuxt-3-tailwind-and-supabase-crash-course-1482)<br/>
|
||||
|
||||
- A sneak peek at the Supabase and Next.js RSC SaaS kit. [Article](https://makerkit.dev/blog/changelog/nextjs-supabase-kit-update)<br/>
|
||||
|
||||
- Build a Fullstack serverless application using React, Chakra UI, Hookstate, and Supabase. [Tutorial](https://clx.hashnode.dev/build-a-fullstack-serverless-application-using-react-chakra-ui-hookstate-and-supabase)<br/>
|
||||
|
||||
- Implement Sign in with Google using Supabase Auth in NextJS. [Tutorial](https://dev.to/irwanphan/implement-sign-in-with-google-using-supabase-auth-in-nextjs-1jj1)<br/>
|
||||
|
||||
## We're Hiring
|
||||
|
||||
Come join one of the fastest-growing open source projects ever 🤗
|
||||
|
||||
- [Technical partnerships lead (Next.js, Node,js, Supabase)](https://boards.greenhouse.io/supabase/jobs/4775849004)\
|
||||
- [Junior Developer Advocate (SMM & Community)](https://boards.greenhouse.io/supabase/jobs/4777008004)
|
||||
|
||||
## Meme Zone
|
||||
|
||||
As always, one of our favorite memes from last month. [Follow us on Twitter](https://twitter.com/supabase) for more.
|
||||
|
||||

|
||||
|
||||
## It's happenning! Coming in April
|
||||
|
||||

|
||||
@@ -16,18 +16,22 @@ const BuiltExamples = () => {
|
||||
<p className="p">There are many example apps and starter projects to get going</p>
|
||||
<div className="flex justify-center gap-2 py-4">
|
||||
<Link href="/docs/guides/examples" as="/docs/guides/examples" passHref>
|
||||
<Button as="a" type="default" size="small">
|
||||
View all examples
|
||||
</Button>
|
||||
<a>
|
||||
<Button type="default" size="small">
|
||||
View all examples
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/supabase/supabase/tree/master/examples"
|
||||
as="https://github.com/supabase/supabase/tree/master/examples"
|
||||
passHref
|
||||
>
|
||||
<Button as="a" type="default" icon={<IconGitHub />} size="small">
|
||||
Official github library
|
||||
</Button>
|
||||
<a>
|
||||
<Button type="default" icon={<IconGitHub />} size="small">
|
||||
Official github library
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loaded 100 of 729 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user