mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
154 files changed
+6291
-1284
No files matched your search
+2
-1
@@ -3,4 +3,5 @@
|
||||
node_modules
|
||||
package-lock.json
|
||||
docker*
|
||||
apps/**/out
|
||||
apps/**/out
|
||||
**/**.mdx
|
||||
@@ -0,0 +1,8 @@
|
||||
# PUBLIC
|
||||
NEXT_PUBLIC_NEW_DOCS=true
|
||||
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||
|
||||
# PRIVATE
|
||||
OPENAI_KEY=
|
||||
SUPABASE_SERVICE_ROLE_KEY=
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useTheme } from 'common/Providers'
|
||||
import Image from 'next/image'
|
||||
import { FC } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import clippyImageDark from '../../public/img/clippy-dark.png'
|
||||
import clippyImage from '../../public/img/clippy.png'
|
||||
import ClippyBubble from './ClippyBubble'
|
||||
import ClippyModal from './ClippyModal'
|
||||
import { useClippy } from './ClippyProvider'
|
||||
import { useFlag } from '~/hooks/useFlag'
|
||||
const Clippy: FC = () => {
|
||||
const { isDarkMode } = useTheme()
|
||||
const { isOpen, open, close } = useClippy()
|
||||
const enableClippy = useFlag('enableClippy')
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isOpen && (
|
||||
<div className="hidden md:flex flex-col items-end gap-1 md:gap-4">
|
||||
{enableClippy && (
|
||||
<>
|
||||
<ClippyBubble onClick={open} />
|
||||
<div className="w-[80px] lg:w-[120px] p-8 md:p-0">
|
||||
<Image src={isDarkMode ? clippyImageDark : clippyImage} alt="Clippy" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isOpen && createPortal(<ClippyModal onClose={close} />, document.body)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Clippy
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FC } from 'react'
|
||||
import { IconCommand } from 'ui'
|
||||
import Image from 'next/image'
|
||||
interface Props {
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const ClippyBubble: FC<Props> = ({ className, onClick }) => {
|
||||
return (
|
||||
<div
|
||||
className={`flex gap-2 border bg-white dark:bg-scale-300 relative rounded-xl p-4 lg:p-6 max-w-sm lg:max-w-xs shadow-xl mb-1 lg:mb-4 cursor-pointer hover:bg-scale-200 transition-colors border-scale-500 hover:border-scale-700 overflow-hidden ${
|
||||
className ? className : ''
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="z-0 absolute -left-24 -top-4 w-[260px]">
|
||||
<Image alt="Clippy blur" width={260} height={260} src="/docs/img/purple-glow-dark.png" />
|
||||
</div>
|
||||
What can I help you with?
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="text-scale-1200 flex items-center justify-center h-5 w-10 border rounded bg-scale-500 border-scale-700 gap-1">
|
||||
<IconCommand size={12} strokeWidth={1.5} />
|
||||
<span className="text-[12px]">/</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -bottom-4 right-[90px] text-scale-600">
|
||||
<svg viewBox="0 0 1 1" width={16} fill="currentColor">
|
||||
<path d="M0,0 L1,0 L1,1 Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ClippyBubble
|
||||
@@ -0,0 +1,267 @@
|
||||
import type { CreateCompletionResponse } from 'openai'
|
||||
import { FC, useCallback, 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 { useTheme } from 'common/Providers'
|
||||
import Image from 'next/image'
|
||||
import {
|
||||
Button,
|
||||
IconAlertCircle,
|
||||
IconAlertTriangle,
|
||||
IconLoader,
|
||||
IconSearch,
|
||||
Input,
|
||||
Loading,
|
||||
Modal,
|
||||
} from 'ui'
|
||||
import components from '~/components'
|
||||
|
||||
type Props = {
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
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 setup authentication?',
|
||||
]
|
||||
|
||||
function getEdgeFunctionUrl() {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL.replace(/\/$/, '')
|
||||
const isPlatform = supabaseUrl.match(/(supabase\.co)|(supabase\.in)/)
|
||||
|
||||
if (isPlatform) {
|
||||
const [schemeAndProjectId, domain, tld] = supabaseUrl.split('.')
|
||||
return `${schemeAndProjectId}.functions.${domain}.${tld}`
|
||||
} else {
|
||||
return `${supabaseUrl}/functions/v1`
|
||||
}
|
||||
}
|
||||
|
||||
const edgeFunctionUrl = getEdgeFunctionUrl()
|
||||
|
||||
const ClippyModal: FC<Props> = ({ onClose }) => {
|
||||
const { isDarkMode } = useTheme()
|
||||
const [query, setQuery] = useState('')
|
||||
const [answer, setAnswer] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isResponding, setIsResponding] = useState(false)
|
||||
const [hasError, setHasError] = useState(false)
|
||||
|
||||
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 || hasError
|
||||
? 'Clippy has failed you'
|
||||
: undefined
|
||||
|
||||
const handleConfirm = useCallback(async (query: string) => {
|
||||
setHasError(false)
|
||||
setAnswer(undefined)
|
||||
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)
|
||||
setHasError(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()
|
||||
|
||||
setIsLoading(true)
|
||||
}, [])
|
||||
|
||||
function handleResetPrompt() {
|
||||
setQuery('')
|
||||
setAnswer(undefined)
|
||||
setIsResponding(false)
|
||||
setHasError(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal size="xlarge" visible={true} onCancel={onClose} closable={false} hideFooter>
|
||||
<div
|
||||
className={`mx-auto 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="Ask me anything about Supabase"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
icon={<IconSearch size="small" />}
|
||||
onKeyDown={(e) => {
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
handleConfirm(query)
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="absolute right-0 top-0 mt-3 mr-4 hidden md:block">
|
||||
<Button type="default" size="tiny" onClick={onClose}>
|
||||
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>
|
||||
|
||||
{!isLoading && !answer && !hasError && (
|
||||
<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) => (
|
||||
<li>
|
||||
<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)
|
||||
handleConfirm(question)
|
||||
}}
|
||||
>
|
||||
{question}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{answer && (
|
||||
<div className="px-4 py-4 rounded-lg overflow-y-auto bg-scale-200">
|
||||
{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>
|
||||
)}
|
||||
{hasError && (
|
||||
<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/clippy" 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>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ClippyModal
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
export type ClippyContextValue = {
|
||||
isOpen: boolean
|
||||
open: () => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export const ClippyContext = createContext<ClippyContextValue>(null)
|
||||
|
||||
export const useClippy = () => {
|
||||
const { isOpen, open, close } = useContext(ClippyContext)
|
||||
|
||||
return { isOpen, open, close }
|
||||
}
|
||||
|
||||
const ClippyProvider: FC<PropsWithChildren<{}>> = ({ children }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
setIsModalOpen(true)
|
||||
document.body.classList.add('DocSearch--active')
|
||||
}, [])
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
setIsModalOpen(false)
|
||||
document.body.classList.remove('DocSearch--active')
|
||||
}, [])
|
||||
|
||||
useSearchKeyboardEvents({
|
||||
onOpen,
|
||||
onClose,
|
||||
})
|
||||
return (
|
||||
<ClippyContext.Provider value={{ isOpen: isModalOpen, open: onOpen, close: onClose }}>
|
||||
{children}
|
||||
</ClippyContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function useSearchKeyboardEvents({ onOpen, onClose }) {
|
||||
useEffect(() => {
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
onClose()
|
||||
return
|
||||
case '/':
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
onOpen()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}, [onOpen, onClose])
|
||||
}
|
||||
|
||||
export default ClippyProvider
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC } from 'react'
|
||||
import { Children, FC } from 'react'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import monokaiCustomTheme from './CodeBlock.utils'
|
||||
@@ -49,10 +49,18 @@ const CodeBlock: FC<Props> = ({
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Extract string when `children` has a single string node
|
||||
const childrenArray = Children.toArray(children)
|
||||
const [singleChild] = childrenArray.length === 1 ? childrenArray : []
|
||||
const singleString = typeof singleChild === 'string' ? singleChild : undefined
|
||||
|
||||
let codeValue = value ?? singleString ?? children
|
||||
codeValue = codeValue?.trimEnd?.() ?? codeValue
|
||||
|
||||
// check the length of the string inside the <code> tag
|
||||
// if it's fewer than 70 characters, add a white-space: pre so it doesn't wrap
|
||||
const shortCodeBlockClasses =
|
||||
typeof children === 'string' && children.length < 70 ? 'short-inline-codeblock' : ''
|
||||
typeof codeValue === 'string' && codeValue.length < 70 ? 'short-inline-codeblock' : ''
|
||||
|
||||
let lang = language ? language : className ? className.replace('language-', '') : 'js'
|
||||
// force jsx to be js highlighted
|
||||
@@ -118,7 +126,7 @@ const CodeBlock: FC<Props> = ({
|
||||
paddingBottom: '4px',
|
||||
}}
|
||||
>
|
||||
{(value || children)?.trimEnd()}
|
||||
{codeValue}
|
||||
</SyntaxHighlighter>
|
||||
{!hideCopy && (value || children) && className ? (
|
||||
<div
|
||||
@@ -128,7 +136,7 @@ const CodeBlock: FC<Props> = ({
|
||||
`${!title ? 'top-2' : 'top-[3.25rem]'}`,
|
||||
].join(' ')}
|
||||
>
|
||||
{/* //
|
||||
{/* //
|
||||
@ts-ignore */}
|
||||
<CopyToClipboard text={value || children}>
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, { ReactNode } from 'react'
|
||||
import { useFlag } from '~/hooks/useFlag'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
name: string
|
||||
}
|
||||
|
||||
function Flag({ children, name }: Props) {
|
||||
const flagValue = useFlag(name)
|
||||
if (!flagValue) {
|
||||
return null
|
||||
}
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
export default Flag
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
const FlagContext = createContext({})
|
||||
|
||||
export default FlagContext
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import createConfigCatClient from 'configcat-js'
|
||||
import FlagContext from './FlagContext'
|
||||
|
||||
const FlagProvider: FC = ({ children }) => {
|
||||
const [store, setStore] = useState({})
|
||||
|
||||
const { Provider } = FlagContext
|
||||
|
||||
useEffect(() => {
|
||||
getFlags()
|
||||
}, [])
|
||||
|
||||
const getFlags = async () => {
|
||||
const setFlagValues = async () => {
|
||||
const flagValues = await client.getAllValuesAsync()
|
||||
const flagStore: any = {}
|
||||
|
||||
flagValues.forEach((item: any) => {
|
||||
flagStore[item.settingKey] = item.settingValue
|
||||
})
|
||||
setStore(flagStore)
|
||||
}
|
||||
|
||||
const client = createConfigCatClient(process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '', {
|
||||
configChanged: setFlagValues,
|
||||
pollIntervalSeconds: 600,
|
||||
})
|
||||
|
||||
await setFlagValues()
|
||||
}
|
||||
|
||||
return <Provider value={store}>{children}</Provider>
|
||||
}
|
||||
|
||||
export default FlagProvider
|
||||
@@ -1,12 +1,38 @@
|
||||
const FooterHelpCallout = () => {
|
||||
import { ReactMarkdown } from 'react-markdown/lib/react-markdown'
|
||||
|
||||
export type FooterHelpCalloutType = 'default' | 'postgres'
|
||||
|
||||
const content = {
|
||||
default: {
|
||||
title: 'Need some help?',
|
||||
description: `Not to worry, our specialist engineers are here to help. Submit a support ticket through the [Dashboard](https://app.supabase.com/support/new).`,
|
||||
},
|
||||
postgres: {
|
||||
title: 'Looking for Serverless Postgres?',
|
||||
description: `Supabase is the fastest way to get started with Postgres in a serverless environment. [Learn more](https://supabase.com/database?utm=postgres-helpers).`,
|
||||
},
|
||||
}
|
||||
|
||||
const FooterHelpCallout = ({
|
||||
footerHelpType = 'default',
|
||||
title,
|
||||
}: {
|
||||
footerHelpType: FooterHelpCalloutType
|
||||
title: any
|
||||
}) => {
|
||||
return (
|
||||
<div className="mt-32">
|
||||
<div className="min-w-full px-8 py-6 prose bg-scale-300 dark:bg-whiteA-200 rounded">
|
||||
<h5 className="text-sm m-0 text-scale-1100">Need some help?</h5>
|
||||
<p className="m-0 text-sm text-scale-900">
|
||||
Not to worry, our specialist engineers are here to help. Submit a support ticket through
|
||||
the <a href="https://app.supabase.com/support/new">Dashboard</a>.
|
||||
</p>
|
||||
<div className="mt-32 prose prose--remove-p-margin max-w-none">
|
||||
<div
|
||||
className="
|
||||
min-w-full
|
||||
px-8 py-6
|
||||
bg-scale-300 dark:bg-whiteA-200
|
||||
rounded
|
||||
text-sm text-scale-900
|
||||
"
|
||||
>
|
||||
<h5 className="text-sm text-scale-1100 m-0">{content[footerHelpType].title}</h5>
|
||||
<ReactMarkdown>{content[footerHelpType].description}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -32,7 +32,12 @@ or you can just copy/paste the SQL from below and run it yourself.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/sql-user-management-starter.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabPanel>
|
||||
@@ -53,5 +58,10 @@ We just need to get the URL and `anon` key from the API settings.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
@@ -243,6 +243,8 @@ export const menuItems: NavMenu = {
|
||||
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: [] },
|
||||
],
|
||||
|
||||
@@ -388,22 +388,52 @@ export const database = {
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/database/extensions', items: [] },
|
||||
{
|
||||
name: 'HypoPG: Hypothetical indexes',
|
||||
url: '/guides/database/extensions/hypopg',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'plv8: Javascript Language',
|
||||
url: '/guides/database/extensions/plv8',
|
||||
items: [],
|
||||
},
|
||||
{ name: 'http: RESTful Client', url: '/guides/database/extensions/http', items: [] },
|
||||
{
|
||||
name: 'PGRoonga: Multilingual Full Text Search',
|
||||
url: '/guides/database/extensions/pgroonga',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'pg_cron: Job Scheduling',
|
||||
url: '/guides/database/extensions/pgcron',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'pg_jsonschema: JSON Schema Validation',
|
||||
url: '/guides/database/extensions/pg_jsonschema',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'pg_net: Async Networking',
|
||||
url: '/guides/database/extensions/pgnet',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'pgvector: Embeddings and vector similarity',
|
||||
url: '/guides/database/extensions/pgvector',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'pg_stat_statements: SQL Planning and Execution Statistics',
|
||||
url: '/guides/database/extensions/pg_stat_statements',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'pg_repack: Storage Optimization',
|
||||
url: '/guides/database/extensions/pgrepack',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'PostGIS: Geo queries',
|
||||
url: '/guides/database/extensions/postgis',
|
||||
@@ -415,6 +445,11 @@ export const database = {
|
||||
url: '/guides/database/extensions/uuid-ossp',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'RUM: inverted index for full-text search',
|
||||
url: '/guides/database/extensions/rum',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -479,6 +514,8 @@ export const realtime = {
|
||||
name: 'Channels',
|
||||
url: undefined,
|
||||
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: [] },
|
||||
],
|
||||
@@ -593,8 +630,7 @@ export const resources = {
|
||||
label: 'Resources',
|
||||
url: '/guides/resources',
|
||||
items: [
|
||||
// removing until the examples page is reworked
|
||||
// { name: 'Examples', url: '/guides/resources/examples', items: [] },
|
||||
{ name: 'Examples', url: '/guides/resources/examples', items: [] },
|
||||
{ name: 'Glossary', url: '/guides/resources/glossary', items: [] },
|
||||
{
|
||||
name: 'Migrate to Supabase',
|
||||
@@ -622,6 +658,24 @@ export const resources = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Postgres resources',
|
||||
url: undefined,
|
||||
items: [
|
||||
{
|
||||
name: 'Drop all tables in schema',
|
||||
url: '/guides/resources/postgres/dropping-all-tables-in-schema',
|
||||
},
|
||||
{
|
||||
name: 'Select first row per group',
|
||||
url: '/guides/resources/postgres/first-row-in-group',
|
||||
},
|
||||
{
|
||||
name: 'Print PostgreSQL version',
|
||||
url: '/guides/resources/postgres/which-version-of-postgres',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -763,7 +817,7 @@ export const reference = {
|
||||
url: '/reference/python/start',
|
||||
level: 'reference_python',
|
||||
items: [],
|
||||
icon: 'docs/img/icons/javascript.svg',
|
||||
icon: '/img/icons/menu/reference-python',
|
||||
},
|
||||
// {
|
||||
// name: 'supabase-python',
|
||||
|
||||
@@ -12,8 +12,17 @@ const NavigationMenuGuideList: React.FC<Props> = ({ id, active }) => {
|
||||
|
||||
// get url
|
||||
const url = router.asPath
|
||||
// remove the end of the url if a deep link
|
||||
const firstLevelRoute = url?.split('/')?.slice(0, 4)?.join('/')
|
||||
|
||||
// We need to decide how deep we want the menu to be for matching urls
|
||||
// if the links are really deep, we don't want to match all the way out
|
||||
// But we need to reach out further to make the structure of /resources/postgres/ work
|
||||
// look at /resources/postgres/ vs /auth/phone-login for how these are different
|
||||
let firstLevelRoute
|
||||
if (url.includes('resources/postgres/')) {
|
||||
firstLevelRoute = url?.split('/')?.slice(0, 5)?.join('/')
|
||||
} else {
|
||||
firstLevelRoute = url?.split('/')?.slice(0, 4)?.join('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion.Root
|
||||
|
||||
@@ -6,13 +6,17 @@ import { Button, IconCommand, IconGitHub, IconMoon, IconSearch, IconSun } from '
|
||||
import { SearchButton } from '~/components/DocSearch'
|
||||
import { REFERENCES } from '~/components/Navigation/Navigation.constants'
|
||||
import { useTheme } from 'common/Providers'
|
||||
import clippyImageDark from '../../../public/img/clippy-dark.png'
|
||||
import clippyImage from '../../../public/img/clippy.png'
|
||||
|
||||
import { getPageType } from '~/lib/helpers'
|
||||
import { useClippy } from '~/components/Clippy/ClippyProvider'
|
||||
|
||||
const TopNavBarRef: FC = () => {
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const { open: openClippy } = useClippy()
|
||||
|
||||
const { asPath, push } = useRouter()
|
||||
const pathSegments = asPath.split('/')
|
||||
@@ -84,8 +88,17 @@ const TopNavBarRef: FC = () => {
|
||||
{/* {router.asPath.includes('/reference/') && <RefSwitcher />} */}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-12">
|
||||
<SearchButton className="w-full lg:w-96">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex md:hidden cursor-pointer">
|
||||
<Image
|
||||
onClick={openClippy}
|
||||
width={26}
|
||||
height={29}
|
||||
src={isDarkMode ? clippyImageDark : clippyImage}
|
||||
alt="Clippy"
|
||||
/>
|
||||
</div>
|
||||
<SearchButton className="md:w-full lg:w-96">
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
|
||||
@@ -481,5 +481,19 @@
|
||||
"default_version": "1.1",
|
||||
"installed_version": null,
|
||||
"comment": "XPath querying and XSLT"
|
||||
},
|
||||
{
|
||||
"name": "rum",
|
||||
"schema": "extensions",
|
||||
"default_version": "1.3.13",
|
||||
"installed_version": "1.3.13",
|
||||
"comment": "GIN-like index for text search"
|
||||
},
|
||||
{
|
||||
"name": "pg_repack",
|
||||
"schema": "extensions",
|
||||
"default_version": "1.4.8",
|
||||
"installed_version": "1.4.8",
|
||||
"comment": "optimize physical storage and remove bloat from tables and indexes"
|
||||
}
|
||||
]
|
||||
@@ -1,122 +0,0 @@
|
||||
---
|
||||
id: initializing
|
||||
title: 'Initializing'
|
||||
slug: initializing
|
||||
custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supabase.yml
|
||||
---
|
||||
|
||||
### Initializing a new Client
|
||||
|
||||
<RefSubLayout.EducationRow>
|
||||
<RefSubLayout.Details>
|
||||
|
||||
Initializing a new client is pretty straightforward. Find your project url and public key from the
|
||||
admin panel and pass it into your client initialization function.
|
||||
|
||||
`supabase-csharp` is heavily dependent on Models deriving from `BaseModel`. To interact with the API, one must have the associated model (see example) specified.
|
||||
|
||||
Leverage `Table`, `PrimaryKey`, and `Column` attributes to specify names of classes/properties that are different from their C# Versions.
|
||||
|
||||
</RefSubLayout.Details>
|
||||
|
||||
<RefSubLayout.Examples>
|
||||
|
||||
<Tabs
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="csharp-init-standard">
|
||||
<TabPanel id="csharp-init-standard" label="Standard">
|
||||
|
||||
```csharp
|
||||
var url = Environment.GetEnvironmentVariable("SUPABASE_URL");
|
||||
var key = Environment.GetEnvironmentVariable("SUPABASE_KEY");
|
||||
|
||||
var options = new Supabase.SupabaseOptions
|
||||
{
|
||||
AutoConnectRealtime = true
|
||||
};
|
||||
|
||||
var supabase = new Supabase.Client(url, key, options);
|
||||
await supabase.InitializeAsync();
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="csharp-init-maui" label="Dependency Injection (Maui-like)">
|
||||
|
||||
```csharp
|
||||
public static MauiApp CreateMauiApp()
|
||||
{
|
||||
// ...
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
|
||||
var url = Environment.GetEnvironmentVariable("SUPABASE_URL");
|
||||
var key = Environment.GetEnvironmentVariable("SUPABASE_KEY");
|
||||
var options = new SupabaseOptions
|
||||
{
|
||||
AutoRefreshToken = true,
|
||||
AutoConnectRealtime = true,
|
||||
SessionHandler = new SupabaseSessionHandler()
|
||||
};
|
||||
|
||||
// Note the creation as a singleton.
|
||||
builder.Services.AddSingleton(provider => new Supabase.Client(url, key, options));
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="csharp-init-showing-models" label="With Models Example">
|
||||
|
||||
```csharp
|
||||
// Given the following Model representing the Supabase Database (Message.cs)
|
||||
[Table("messages")]
|
||||
public class Message : BaseModel
|
||||
{
|
||||
[PrimaryKey("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("username")]
|
||||
public string UserName { get; set; }
|
||||
|
||||
[Column("channel_id")]
|
||||
public int ChannelId { get; set; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is Message message &&
|
||||
Id == message.Id;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Id);
|
||||
}
|
||||
}
|
||||
|
||||
void Initialize()
|
||||
{
|
||||
// Get All Messages
|
||||
var response = await client.Table<Message>().Get();
|
||||
List<Message> models = response.Models;
|
||||
|
||||
// Insert
|
||||
var newMessage = new Message { UserName = "acupofjose", ChannelId = 1 };
|
||||
await client.Table<Message>().Insert();
|
||||
|
||||
// Update
|
||||
var model = response.Models.First();
|
||||
model.UserName = "elrhomariyounes";
|
||||
await model.Update();
|
||||
|
||||
// Delete
|
||||
await response.Models.Last().Delete();
|
||||
|
||||
// etc.
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
</RefSubLayout.Examples>
|
||||
</RefSubLayout.EducationRow>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useContext } from 'react'
|
||||
import FlagContext from 'components/Flag/FlagContext'
|
||||
|
||||
export function useFlag(name: string) {
|
||||
const store: any = useContext(FlagContext)
|
||||
return store[name]
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
import { FC } from 'react'
|
||||
import { FooterHelpCalloutType } from '~/components/FooterHelpCallout'
|
||||
import GuideLayout from './guides'
|
||||
|
||||
interface Props {
|
||||
meta: { title: string; description?: string; hide_table_of_contents?: boolean; video?: string }
|
||||
meta: {
|
||||
title: string
|
||||
description?: string
|
||||
hide_table_of_contents?: boolean
|
||||
video?: string
|
||||
footerHelpType?: FooterHelpCalloutType
|
||||
}
|
||||
children: any
|
||||
toc?: any
|
||||
currentPage?: string
|
||||
|
||||
@@ -7,6 +7,7 @@ import TopNavBarRef from '~/components/Navigation/NavigationMenu/TopNavBarRef'
|
||||
import FooterHelpCallout from '~/components/FooterHelpCallout'
|
||||
|
||||
import { memo, useEffect } from 'react'
|
||||
import Clippy from '~/components/Clippy/Clippy'
|
||||
import Footer from '~/components/Navigation/Footer'
|
||||
import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState'
|
||||
|
||||
@@ -334,11 +335,13 @@ const SiteLayout = ({ children }) => {
|
||||
</div>
|
||||
<div className="grow px-5 max-w-7xl mx-auto py-16">
|
||||
{children}
|
||||
<FooterHelpCallout />
|
||||
<Footer />
|
||||
</div>
|
||||
<MobileMenuBackdrop />
|
||||
</Container>
|
||||
<div className=" absolute right-4 md:right-12 bottom-8 z-[300]">
|
||||
<Clippy />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { IconExternalLink } from 'ui'
|
||||
import components from '~/components'
|
||||
import { highlightSelectedTocItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils'
|
||||
import FooterHelpCallout, { FooterHelpCalloutType } from '~/components/FooterHelpCallout'
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import useHash from '~/hooks/useHash'
|
||||
|
||||
@@ -16,6 +17,7 @@ interface Props {
|
||||
hide_table_of_contents?: boolean
|
||||
breadcrumb?: string
|
||||
subtitle?: string
|
||||
footerHelpType?: FooterHelpCalloutType
|
||||
video?: string
|
||||
canonical?: string
|
||||
}
|
||||
@@ -161,6 +163,7 @@ const Layout: FC<Props> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FooterHelpCallout footerHelpType={props.meta?.footerHelpType} title={props.meta?.title} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,6 @@ const nextConfig = {
|
||||
// next.config.js
|
||||
export default () => {
|
||||
// const plugins = [withMDX]/
|
||||
const plugins = [withTM(['ui', 'common']), withMDX, withYaml]
|
||||
const plugins = [withTM(['ui', 'common', '@supabase/auth-helpers-nextjs']), withMDX, withYaml]
|
||||
return plugins.reduce((acc, next) => next(acc), nextConfig)
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"build:sitemap": "node ./internals/generate-sitemap.mjs",
|
||||
"build:embeddings": "tsx scripts/generate-embeddings.ts",
|
||||
"postbuild": "ts-node ./scripts/build-search.ts && node ./internals/generate-sitemap.mjs",
|
||||
"generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-csharp:v0 gen:supabase-js:v1 gen:supabase-js:v2 gen:realtime",
|
||||
"gen:api": "npm-run-all gen:api:usage",
|
||||
@@ -42,6 +43,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-react": "^0.3.1",
|
||||
"@supabase/supabase-js": "^2.4.1",
|
||||
"algoliasearch": "^4.14.2",
|
||||
"babel": "^6.23.0",
|
||||
"clsx": "^1.2.1",
|
||||
@@ -52,7 +56,12 @@
|
||||
"jsrsasign": "^10.5.26",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-toc": "^1.2.0",
|
||||
"mdast": "^3.0.0",
|
||||
"mdast-util-from-markdown": "^1.2.0",
|
||||
"mdast-util-mdx": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^1.5.0",
|
||||
"mdx-mermaid": "2.0.0-rc3",
|
||||
"micromark-extension-mdxjs": "^1.0.0",
|
||||
"next": "12.3.2",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
"next-mdx-remote": "^4.1.0",
|
||||
@@ -60,6 +69,7 @@
|
||||
"next-plugin-yaml": "^1.0.1",
|
||||
"next-seo": "^5.14.1",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"openai": "^3.1.0",
|
||||
"react": "17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
@@ -72,7 +82,10 @@
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-slug": "^7.0.1",
|
||||
"shiki": "^0.11.1",
|
||||
"sse.js": "^0.6.1",
|
||||
"ui": "*",
|
||||
"unist-builder": "^3.0.1",
|
||||
"unist-util-filter": "^4.0.1",
|
||||
"valtio": "^1.7.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -91,6 +104,7 @@
|
||||
"sass": "^1.55.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig": "*",
|
||||
"tsx": "^3.12.2",
|
||||
"typescript": "^4.5.3"
|
||||
}
|
||||
}
|
||||
+44
-33
@@ -1,21 +1,26 @@
|
||||
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 { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AppPropsWithLayout } from 'types'
|
||||
import ClippyProvider from '~/components/Clippy/ClippyProvider'
|
||||
import { SearchProvider } from '~/components/DocSearch'
|
||||
import Favicons from '~/components/Favicons'
|
||||
import { ThemeProvider } from 'common/Providers'
|
||||
import SiteLayout from '~/layouts/SiteLayout'
|
||||
import { post } from '~/lib/fetchWrappers'
|
||||
import FlagProvider from 'components/Flag/FlagProvider'
|
||||
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'
|
||||
import { post } from '~/lib/fetchWrappers'
|
||||
|
||||
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
const router = useRouter()
|
||||
const [supabase] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
function telemetry(route: string) {
|
||||
return post(`https://api.supabase.io/platform/telemetry/page`, {
|
||||
@@ -58,36 +63,42 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Favicons />
|
||||
<DefaultSeo
|
||||
title={SITE_TITLE}
|
||||
description={SITE_DESCRIPTION}
|
||||
openGraph={{
|
||||
type: 'website',
|
||||
url: 'https://supabase.com/docs',
|
||||
site_name: SITE_TITLE,
|
||||
images: [
|
||||
{
|
||||
url: `https://supabase.com${basePath}/img/supabase-og-image.png`,
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: 'Supabase Og Image',
|
||||
},
|
||||
],
|
||||
}}
|
||||
twitter={{
|
||||
handle: '@supabase',
|
||||
site: '@supabase',
|
||||
cardType: 'summary_large_image',
|
||||
}}
|
||||
/>
|
||||
<ThemeProvider>
|
||||
<SearchProvider>
|
||||
<SiteLayout>
|
||||
<Component {...pageProps} />
|
||||
</SiteLayout>
|
||||
</SearchProvider>
|
||||
</ThemeProvider>
|
||||
<FlagProvider>
|
||||
<Favicons />
|
||||
<DefaultSeo
|
||||
title={SITE_TITLE}
|
||||
description={SITE_DESCRIPTION}
|
||||
openGraph={{
|
||||
type: 'website',
|
||||
url: 'https://supabase.com/docs',
|
||||
site_name: SITE_TITLE,
|
||||
images: [
|
||||
{
|
||||
url: `https://supabase.com${basePath}/img/supabase-og-image.png`,
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: 'Supabase Og Image',
|
||||
},
|
||||
],
|
||||
}}
|
||||
twitter={{
|
||||
handle: '@supabase',
|
||||
site: '@supabase',
|
||||
cardType: 'summary_large_image',
|
||||
}}
|
||||
/>
|
||||
<SessionContextProvider supabaseClient={supabase}>
|
||||
<ThemeProvider>
|
||||
<SearchProvider>
|
||||
<ClippyProvider>
|
||||
<SiteLayout>
|
||||
<Component {...pageProps} />
|
||||
</SiteLayout>
|
||||
</ClippyProvider>
|
||||
</SearchProvider>
|
||||
</ThemeProvider>
|
||||
</SessionContextProvider>
|
||||
</FlagProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -85,7 +85,12 @@ This creates a corresponding route `todos` which can accept `GET`, `POST`, `PATC
|
||||
1. Click **Save**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src={'/docs/videos/api/api-create-table-sm.mp4'} type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabPanel>
|
||||
@@ -113,7 +118,12 @@ Every Supabase project has a unique API URL. Your API is secured behind an API g
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src={'/docs/videos/api/api-url-and-key.mp4'} type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
The REST API and the GraphQL API are both accessible through this URL:
|
||||
@@ -142,7 +152,12 @@ Let's view the documentation for a `countries` table which we created in our dat
|
||||
3. Switch between the JavaScript and the cURL docs using the tabs.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src={'/docs/videos/api/api-docs.mp4'} type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-docs.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
#### GraphQL
|
||||
@@ -287,7 +302,12 @@ By default Realtime is disabled on your database. Let's turn on Realtime for the
|
||||
4. Control which tables broadcast changes by selecting **Source** and toggling each table.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src={'/docs/videos/api/api-realtime.mp4'} type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-realtime.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabPanel>
|
||||
|
||||
@@ -34,10 +34,7 @@ Pass `supabaseClient` from `@supabase/supabase-js` as a prop to the component.
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
|
||||
const supabase = createClient(
|
||||
'<INSERT PROJECT URL>',
|
||||
'<INSERT PROJECT ANON API KEY>'
|
||||
)
|
||||
const supabase = createClient('<INSERT PROJECT URL>', '<INSERT PROJECT ANON API KEY>')
|
||||
|
||||
const App = () => <Auth supabaseClient={supabase} />
|
||||
```
|
||||
@@ -75,20 +72,29 @@ The Auth component also supports login with [offical social providers](../../aut
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
|
||||
const supabase = createClient(
|
||||
'<INSERT PROJECT URL>',
|
||||
'<INSERT PROJECT ANON API KEY>'
|
||||
)
|
||||
const supabase = createClient('<INSERT PROJECT URL>', '<INSERT PROJECT ANON API KEY>')
|
||||
|
||||
const App = () => (
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
providers={['google', 'facebook', 'twitter']}
|
||||
providers={['google', 'facebook', 'twitter']}
|
||||
/>
|
||||
)
|
||||
```
|
||||
|
||||
### Supported Views
|
||||
|
||||
The Auth component is currently shipped with the following views:
|
||||
|
||||
- [Email Login](../auth-email)
|
||||
- [Magic Link login](../auth-magic-link)
|
||||
- [Social Login](../social-login)
|
||||
- Update password
|
||||
- Forgotten password
|
||||
|
||||
We are planning on adding more views in the future. Follow along on that [repo](https://github.com/supabase/auth-ui).
|
||||
|
||||
## Customization
|
||||
|
||||
There are several ways to customize Auth UI:
|
||||
@@ -161,10 +167,7 @@ Auth UI themes can be overridden using variable tokens. See the [list of variabl
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
|
||||
const supabase = createClient(
|
||||
'<INSERT PROJECT URL>',
|
||||
'<INSERT PROJECT ANON API KEY>'
|
||||
)
|
||||
const supabase = createClient('<INSERT PROJECT URL>', '<INSERT PROJECT ANON API KEY>')
|
||||
|
||||
const App = () => (
|
||||
<Auth
|
||||
@@ -247,10 +250,7 @@ You can use custom CSS classes for the following elements:
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
|
||||
const supabase = createClient(
|
||||
'<INSERT PROJECT URL>',
|
||||
'<INSERT PROJECT ANON API KEY>'
|
||||
)
|
||||
const supabase = createClient('<INSERT PROJECT URL>', '<INSERT PROJECT ANON API KEY>')
|
||||
|
||||
const App = () => (
|
||||
<Auth
|
||||
@@ -275,10 +275,7 @@ You can use custom CSS inline styles for the following elements:
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
|
||||
const supabase = createClient(
|
||||
'<INSERT PROJECT URL>',
|
||||
'<INSERT PROJECT ANON API KEY>'
|
||||
)
|
||||
const supabase = createClient('<INSERT PROJECT URL>', '<INSERT PROJECT ANON API KEY>')
|
||||
|
||||
const App = () => (
|
||||
<Auth
|
||||
@@ -302,10 +299,7 @@ You can use custom labels with `localization.variables`. See the [list of labels
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
|
||||
const supabase = createClient(
|
||||
'<INSERT PROJECT URL>',
|
||||
'<INSERT PROJECT ANON API KEY>'
|
||||
)
|
||||
const supabase = createClient('<INSERT PROJECT URL>', '<INSERT PROJECT ANON API KEY>')
|
||||
|
||||
const App = () => (
|
||||
<Auth
|
||||
|
||||
@@ -282,7 +282,7 @@ type SupabaseContext = {
|
||||
supabase: SupabaseClient<Database>
|
||||
}
|
||||
|
||||
const Context = createContext<SupabaseContext>()
|
||||
const Context = createContext<SupabaseContext | undefined>(undefined)
|
||||
|
||||
export default function SupabaseProvider({ children }: { children: React.ReactNode }) {
|
||||
const [supabase] = useState(() => createClient())
|
||||
@@ -294,7 +294,14 @@ export default function SupabaseProvider({ children }: { children: React.ReactNo
|
||||
)
|
||||
}
|
||||
|
||||
export const useSupabase = () => useContext(Context)
|
||||
export const useSupabase = () => {
|
||||
let context = useContext(Context);
|
||||
if (context === undefined) {
|
||||
throw new Error("useSupabase must be used inside SupabaseProvider");
|
||||
} else {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createBrowserSupabaseClient` to add type support to the Supabase client.
|
||||
|
||||
@@ -116,7 +116,12 @@ Get started with our [Row Level Security Guides](/docs/guides/auth/row-level-sec
|
||||
Authentication only gets you so far. When you need granular authorization rules, nothing beats PostgreSQL's [Row Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html). Supabase makes it simple to turn RLS on and off.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/rls-zoom2.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/rls-zoom2.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Policies
|
||||
@@ -124,7 +129,12 @@ Authentication only gets you so far. When you need granular authorization rules,
|
||||
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are PostgreSQL's rule engine. They are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/policies-zoom2.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/policies-zoom2.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
With policies, your database becomes the rules engine. Instead of repetitively filtering your queries, like this ...
|
||||
@@ -172,7 +182,12 @@ Supabase provides multiple endpoints to authenticate and manage your users:
|
||||
When users sign up, Supabase assigns them a unique ID. You can reference this ID anywhere in your database. For example, you might create a `profiles` table referencing `id` in the `auth.users` table using a `user_id` field.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/auth-zoom2.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/auth-zoom2.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -152,10 +152,11 @@ Database changes are managed through "migrations." Database migrations are a com
|
||||
For this guide, create a table called `employees`. In Supabase Studio, navigate to the **SQL Editor** page and run the following SQL command:
|
||||
|
||||
```sql
|
||||
create table employees (
|
||||
id integer primary key generated always as identity,
|
||||
name text
|
||||
);
|
||||
create table
|
||||
employees (
|
||||
id integer primary key generated always as identity,
|
||||
name text
|
||||
);
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
@@ -177,8 +178,9 @@ This creates a new migration named `supabase/migrations/<timestamp>_create_emplo
|
||||
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.
|
||||
|
||||
```sql
|
||||
-- in supabase/seed.sql
|
||||
insert into public.employees (name)
|
||||
-- in supabase/seed.sql
|
||||
insert into
|
||||
public.employees (name)
|
||||
values
|
||||
('Erlich Bachman'),
|
||||
('Richard Hendricks'),
|
||||
@@ -198,9 +200,9 @@ You should now see the contents of `employees` in Studio.
|
||||
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 on local database to make a change
|
||||
alter table
|
||||
employees add department text default 'Hooli';
|
||||
```
|
||||
|
||||
Run the following command to reset the local database:
|
||||
@@ -258,10 +260,6 @@ The local development environment is not as feature-complete as the Supabase Pla
|
||||
- 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.
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -34,7 +34,10 @@ You don't have to be a database expert to start using Supabase. Our table view m
|
||||
Dig into the relationships within your data.
|
||||
|
||||
<video width="99%" loop="" muted="" playsInline="" controls="true">
|
||||
<source src="/docs/videos/relational-drilldown-zoom.mp4" type="video/mp4" />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/relational-drilldown-zoom.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Clone tables
|
||||
@@ -42,7 +45,12 @@ Dig into the relationships within your data.
|
||||
You can duplicate your tables, just like you would inside a spreadsheet.
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source src="/docs/videos/duplicate-tables.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/duplicate-tables.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### The SQL Editor
|
||||
@@ -50,7 +58,12 @@ You can duplicate your tables, just like you would inside a spreadsheet.
|
||||
Supabase comes with a SQL Editor. You can also save your favorite queries to run later!
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source src="/docs/videos/favorites.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/favorites.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Additional features
|
||||
@@ -74,7 +87,12 @@ To expand the functionality of your Postgres database, you can use extensions.
|
||||
You can enable Postgres extensions with the click of a button within the Supabase dashboard.
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source src="/docs/videos/toggle-extensions.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/toggle-extensions.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
[Learn more](/docs/guides/database/extensions) about all the extensions provided on Supabase.
|
||||
|
||||
@@ -45,7 +45,12 @@ You cannot manage the database schema via the API (for security reasons). To do
|
||||
You can find the API URL and Keys in the [Dashboard](https://app.supabase.com/project/_/settings/api).
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
## Direct connections
|
||||
@@ -59,7 +64,12 @@ Every Supabase project provides a full Postgres database. You can connect to the
|
||||
3. Find your Connection Info and Connection String. Direct connections are on port `5432`.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/postgres-connection.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/postgres-connection.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
## Connection Pool
|
||||
@@ -104,7 +114,12 @@ This is the most granular option. Connections are returned to the pool after eve
|
||||
3. Find your Connection Info and Connection String. Connection pooling is on port `6543`.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/connection-pool-config.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/connection-pool-config.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
## Connecting with SSL
|
||||
|
||||
@@ -24,7 +24,12 @@ Supabase has pre-installed some of the most useful open source extensions.
|
||||
3. Enable or disable an extension.
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source src="/docs/videos/toggle-extensions.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/toggle-extensions.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabPanel>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'hypopg',
|
||||
title: 'HypoPG: Hypothetical indexes',
|
||||
description: 'Quickly check if an index can be used without creating it.',
|
||||
}
|
||||
|
||||
`HypoPG` is PostgreSQL extension for creating hypothetical/virtual indexes. HypoPG allows users to rapidly create hypothetical/virtual indexes that have no resource cost (CPU, disk, memory) that are visible to the PostgreSQL query planner.
|
||||
That allows users to quickly search for an index to improve a slow query without waiting for them to build.
|
||||
|
||||
## Usage
|
||||
|
||||
### Enable the extension
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "hypopg" and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
-- Enable the "hypopg" extension
|
||||
create extension hypopg with schema extensions;
|
||||
|
||||
-- Disable the "hypopg" extension
|
||||
drop extension if exists hypopg;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
It's good practice to create the extension within a separate schema (like `extensions`) to keep your database clean.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Speeding up a query
|
||||
|
||||
Given the following table and a simple query to select from the table by id:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
create table account (
|
||||
id int,
|
||||
address text
|
||||
);
|
||||
|
||||
insert into account(id, address)
|
||||
select
|
||||
id,
|
||||
id || ' main street'
|
||||
from
|
||||
generate_series(1, 10000) id;
|
||||
```
|
||||
|
||||
We can generate an explain plan for a description of how the PostgreSQL query planner
|
||||
intends to execute the query.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
explain select * from account where id=1;
|
||||
|
||||
QUERY PLAN
|
||||
-------------------------------------------------------
|
||||
Seq Scan on account (cost=0.00..180.00 rows=1 width=13)
|
||||
Filter: (id = 1)
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
Using HypoPG, we can create a hypothetical index on the `account(id)` column to check if it would be useful to the query planner and then re-run the explain plan.
|
||||
|
||||
Note that the virtual indexes created by HypoPG are only visible in the PostgreSQL connection that they were created in. Supabase connects to PostgreSQL through a connection pooler so the `hypopg_create_index` statement and the `explain` statement should be executed in a single query.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
select * from hypopg_create_index('create index on account(id)');
|
||||
|
||||
explain select * from account where id=1;
|
||||
|
||||
QUERY PLAN
|
||||
------------------------------------------------------------------------------------
|
||||
Index Scan using <13504>btree_account_id on hypo (cost=0.29..8.30 rows=1 width=13)
|
||||
Index Cond: (id = 1)
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
The query plan has changed from a `Seq Scan` to an `Index Scan` using the newly created virtual index, so we may choose to create a real version of the index to improve performance on the target query:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
create index on account(id);
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
- [`hypo_create_index(text)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#create-a-hypothetical-index): A function to create a hypothetical index.
|
||||
- [`hypopg_list_indexes`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A View that lists all hypothetical indexes that have been created.
|
||||
- [`hypopg()`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function that lists all hypothetical indexes that have been created with the same format as pg_index.
|
||||
- [`hypopg_get_index_def(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to display the `create index` statement that would create the index.
|
||||
- [`hypopg_get_relation_size(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to estimate how large a hypothetical index would be.
|
||||
- [`hypopg_drop_index(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to remove a given hypothetical index by oid.
|
||||
- [`hypopg_reset()`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to remove all hypothetical indexes.
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [HypoPG documentation](https://hypopg.readthedocs.io/en/rel1_stable/)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,113 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'pg_jsonschema',
|
||||
title: 'pg_jsonschema: JSON Schema Validation',
|
||||
description: 'Validate json/jsonb with JSON Schema in PostgreSQL.',
|
||||
}
|
||||
|
||||
[JSON Schema](https://json-schema.org) is a language for annotating and validating JSON documents. [`pg_jsonschema`](https://json-schema.org) is a PostgreSQL extension that adds the ability to validate PostgreSQL's built-in `json` and `jsonb` data types against a JSON Schema document.
|
||||
|
||||
## Usage
|
||||
|
||||
### Enable the extension
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pg_jsonschema" and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
-- Enable the "pg_jsonschema" extension
|
||||
create extension pg_jsonschema with schema extensions;
|
||||
|
||||
-- Disable the "pg_jsonschema" extension
|
||||
drop extension if exists pg_jsonschema;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
It's good practice to create the extension within a separate schema (like `extensions`) to keep your database clean.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Functions
|
||||
|
||||
- [`json_matches_schema(schema json, instance json)`](https://github.com/supabase/pg_jsonschema#api): Checks if a `json` _instance_ conforms to a JSON Schema _schema_.
|
||||
- [`jsonb_matches_schema(schema json, instance jsonb)`](https://github.com/supabase/pg_jsonschema#api): Checks if a `jsonb` _instance_ conforms to a JSON Schema _schema_.
|
||||
|
||||
### Validating a document
|
||||
|
||||
Since `pg_jsonschema` exposes its utilities as functions, we can test it with a simple select statement:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
select
|
||||
extensions.json_matches_schema(
|
||||
schema := '{"type": "object"}',
|
||||
instance := '{}'
|
||||
);
|
||||
```
|
||||
|
||||
`pg_jsonschema` is generally used in tandem with a [check constraint](https://www.postgresql.org/docs/current/ddl-constraints.html) as a way to constrain the contents of a json/b field to match a JSON Schema.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
create table customer(
|
||||
id serial primary key,
|
||||
...
|
||||
metadata json,
|
||||
|
||||
check (
|
||||
json_matches_schema(
|
||||
'{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 16
|
||||
}
|
||||
}
|
||||
}
|
||||
}',
|
||||
metadata
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- Example: Valid Payload
|
||||
insert into customer(metadata)
|
||||
values ('{"tags": ["vip", "darkmode-ui"]}');
|
||||
-- Result:
|
||||
-- INSERT 0 1
|
||||
|
||||
-- Example: Invalid Payload
|
||||
insert into customer(metadata)
|
||||
values ('{"tags": [1, 3]}');
|
||||
-- Result:
|
||||
-- ERROR: new row for relation "customer" violates check constraint "customer_metadata_check"
|
||||
-- DETAIL: Failing row contains (2, {"tags": [1, 3]}).
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [`pg_jsonschema` documentation](https://github.com/supabase/pg_jsonschema)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,93 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'pg_stat_statements',
|
||||
title: 'pg_stat_statements: SQL Planning and Execution Statistics',
|
||||
description:
|
||||
'Track planning and execution statistics of all SQL statements executed on the database.',
|
||||
}
|
||||
|
||||
`pg_stat_statements` is a database extension that exposes a view, of the same name, to track statistics about SQL statements executed on the database. The following table shows some of the available statistics and metadata:
|
||||
|
||||
| Column Type | Description |
|
||||
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| userid oid (references pg_authid.oid) | OID of user who executed the statement |
|
||||
| dbid oid (references pg_database.oid) | OID of database in which the statement was executed |
|
||||
| toplevel bool | True if the query was executed as a top-level statement (always true if pg_stat_statements.track is set to top) |
|
||||
| queryid bigint | Hash code to identify identical normalized queries. |
|
||||
| query text | Text of a representative statement |
|
||||
| plans bigint | Number of times the statement was planned (if pg_stat_statements.track_planning is enabled, otherwise zero) |
|
||||
| total_plan_time double precision | Total time spent planning the statement, in milliseconds (if pg_stat_statements.track_planning is enabled, otherwise zero) |
|
||||
| min_plan_time double precision | Minimum time spent planning the statement, in milliseconds (if pg_stat_statements.track_planning is enabled, otherwise zero) |
|
||||
|
||||
A full list of statistics is available in the [pg_stat_statements docs](https://www.postgresql.org/docs/current/pgstatstatements.html).
|
||||
|
||||
## Usage
|
||||
|
||||
### Enable the extension
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pg_stat_statements" and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
-- Enable the "pg_stat_statements" extension
|
||||
create extension pg_stat_statements with schema extensions;
|
||||
|
||||
-- Disable the "pg_stat_statements" extension
|
||||
drop extension if exists pg_stat_statements;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
It's good practice to create the extension within a separate schema (like `extensions`) to keep your database clean.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Inspecting activity
|
||||
|
||||
A common use for `pg_stat_statements` is to track down expensive or slow queries. The `pg_stat_statements` view contains a row for each executed query with statistics inlined. For example, you can leverage the statistics to identify frequently executed and slow queries against a given table.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
select
|
||||
calls,
|
||||
mean_exec_time,
|
||||
max_exec_time,
|
||||
total_exec_time,
|
||||
stddev_exec_time,
|
||||
query,
|
||||
from
|
||||
pg_stat_statements
|
||||
where
|
||||
calls > 1000 -- at least 50 calls
|
||||
and mean_exec_time > 2.0 -- averaging at least 2ms/call
|
||||
and total_exec_time > 60000 -- at least one minute total server time spent
|
||||
and query ilike '%user_in_organization%' -- filter to queries that touch the user_in_organization table
|
||||
order by
|
||||
calls desc
|
||||
```
|
||||
|
||||
From the results, an informed decision about which queries to optimize, index, or adjust can be made.
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [pg_stat_statements documentation](https://www.postgresql.org/docs/current/pgstatstatements.html)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,110 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'pg_repack: Physical storage optimization and maintenance',
|
||||
description:
|
||||
'A tool to remove bloat from tables and indexes and optimize physical data order and physical storage',
|
||||
}
|
||||
|
||||
[pg_repack](https://github.com/reorg/pg_repack) is a PostgreSQL extension to remove bloat from tables and indexes, and optionally restore the physical order of clustered indexes. Unlike CLUSTER and VACUUM FULL it works online, without holding an exclusive lock on the processed tables during processing. pg_repack is efficient to boot, with performance comparable to using CLUSTER directly.
|
||||
|
||||
pg_repack provides the following methods to optimize physical storage:
|
||||
|
||||
- Online CLUSTER: ordering table data by cluster index in a non-blocking way
|
||||
- Ordering table data by specified columns
|
||||
- Online VACUUM FULL: packing rows only in a non-blocking way
|
||||
- Rebuild or relocate only the indexes of a table
|
||||
|
||||
## Requirements
|
||||
|
||||
- Only superusers can use the utility.
|
||||
- Target table must have a PRIMARY KEY, or a UNIQUE total index on a NOT NULL column.
|
||||
- Performing a full-table repack requires free disk space about twice as large as the target table and its indexes.
|
||||
|
||||
## Usage
|
||||
|
||||
### Enable the extension
|
||||
|
||||
Get started with pg_repack by enabling the extension in the Supabase Dashboard.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pg_repack" and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Example: enable the "pg_repack" extension
|
||||
create extension pg_repack with schema extensions;
|
||||
|
||||
-- Example: disable the "pg_repack" extension
|
||||
drop extension if exists pg_repack;
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Syntax
|
||||
|
||||
```sh
|
||||
pg_repack [OPTION]... [DBNAME]
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
It's useful for performance to support tables data ordered on disk and physically remove deleted data that remain
|
||||
otherwise.
|
||||
|
||||
Perform an online CLUSTER of all the clustered tables in the database `db`, and perform an online `VACUUM FULL` of all the non-clustered tables:
|
||||
|
||||
```sh
|
||||
pg_repack db
|
||||
```
|
||||
|
||||
Perform an online `VACUUM FULL` on the tables `table1` and `table2` in the database `db` (an eventual cluster index is ignored):
|
||||
|
||||
```sh
|
||||
pg_repack --no-order --table table1 --table table2 db
|
||||
```
|
||||
|
||||
Moving indexes to a tablespace on a faster volume increases performance of `SELECT` queries using these indexes
|
||||
drastically. `INSERT`s and `UPDATE`s of a table with indexes on a fast volume are also faster. This is very useful
|
||||
when the fast volume is small and can not accommodate all tables, as indexes are much smaller than tables.
|
||||
|
||||
Move all indexes of table `table1` to tablespace `tbs`:
|
||||
|
||||
```sh
|
||||
pg_repack -d db --table table1 --only-indexes --tablespace tbs
|
||||
```
|
||||
|
||||
Move the specified index `idx` to tablespace `tbs`:
|
||||
|
||||
```sh
|
||||
pg_repack -d db --index idx --tablespace tbs
|
||||
```
|
||||
|
||||
See the [official pg_repack documentation](https://reorg.github.io/pg_repack/) for the full list of options.
|
||||
|
||||
## Restrictions
|
||||
|
||||
- pg_repack cannot reorganize temp tables.
|
||||
- pg_repack cannot cluster tables by GiST indexes.
|
||||
- You cannot perform DDL commands of the target tables except VACUUM or ANALYZE while pg_repack is working.
|
||||
pg_repack holds an ACCESS SHARE lock on the target table to enforce this restriction.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Official pg_repack documentation](https://reorg.github.io/pg_repack/)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,113 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'pgroonga',
|
||||
title: 'PGRoonga: Multilingual Full Text Search',
|
||||
description: 'Full Text Search for multiple languages in PostgreSQL',
|
||||
}
|
||||
|
||||
`PGroonga` is a PostgreSQL extension adding a full text search indexing method based on [Groonga](https://groonga.org). While native PostgreSQL supports full text indexing, it is limited to alphabet and digit based languages. `PGroonga` offers a wider range of character support making it viable for a superset of languages supported by PostgreSQL including Japanese, Chinese, etc.
|
||||
|
||||
## Usage
|
||||
|
||||
### Enable the extension
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pgroonga" and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
-- Enable the "pgroonga" extension
|
||||
create extension pgroonga with schema extensions;
|
||||
|
||||
-- Disable the "pgroonga" extension
|
||||
drop extension if exists pgroonga;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
To disable an extension you can call `drop extension`.
|
||||
|
||||
It's good practice to create the extension within a separate schema (like `extensions`) to keep your database clean.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Creating a full text search index
|
||||
|
||||
Given a table with a `text` column:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
create table memos (
|
||||
id serial primary key,
|
||||
content text
|
||||
);
|
||||
```
|
||||
|
||||
We can index the column for full text search with a `pgroonga` index:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
create index ix_memos_content ON memos USING pgroonga(content);
|
||||
```
|
||||
|
||||
To test the full text index, we'll add some data.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
insert into memos(content)
|
||||
values
|
||||
('PostgreSQL is a relational database management system.'),
|
||||
('Groonga is a fast full text search engine that supports all languages.'),
|
||||
('PGroonga is a PostgreSQL extension that uses Groonga as index.'),
|
||||
('There is groonga command.');
|
||||
```
|
||||
|
||||
The PostgreSQL query planner is smart enough to know that, for extremely small tables, it's faster to scan the whole table rather than loading an index. To force the index to be used, we can disable sequential scans:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
-- For testing only. Don't do this in production
|
||||
set enable_seqscan = off;
|
||||
```
|
||||
|
||||
Now if we run an explain plan on a query filtering on `memos.content`:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
explain select * from memos where content like '%engine%';
|
||||
|
||||
QUERY PLAN
|
||||
-----------------------------------------------------------------------------
|
||||
Index Scan using ix_memos_content on memos (cost=0.00..1.11 rows=1 width=36)
|
||||
Index Cond: (content ~~ '%engine%'::text)
|
||||
(2 rows)
|
||||
```
|
||||
|
||||
The pgroonga index is used to retrive the result set:
|
||||
|
||||
```markdown
|
||||
| id | content |
|
||||
| --- | ------------------------------------------------------------------------ |
|
||||
| 2 | 'Groonga is a fast full text search engine that supports all languages.' |
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [PGroonga documentation](https://pgroonga.github.io/tutorial/)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,101 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'pgvector',
|
||||
title: 'pgvector: Embeddings and vector similarity',
|
||||
description:
|
||||
'pgvector: a PostgreSQL extension for storing embeddings and performing vector similarity search.',
|
||||
}
|
||||
|
||||
[pgvector](https://github.com/pgvector/pgvector/) is a PostgreSQL extension for vector similarity search. It can also be used for storing [embeddings](https://en.wikipedia.org/wiki/Word_embedding).
|
||||
|
||||
## Concepts
|
||||
|
||||
### Vector similarity
|
||||
|
||||
Vector similarity refers to a measure of the similarity between two related items. For example, if you have a list of products, you can use vector similarity to find similar products. To do this, you need to convert each product into a "vector" of numbers, using a mathematical model. You can use a similar model for text, images, and other types of data. Once all of these vectors are stored in the database, you can use vector similarity to find similar items.
|
||||
|
||||
### Embeddings
|
||||
|
||||
This is particularly useful if you're building on top of OpenAI's [GPT-3](https://openai.com/blog/gpt-3-apps/). You can create and store [embeddings](https://platform.openai.com/docs/guides/embeddings) which match the GPT model you're using.
|
||||
|
||||
## Usage
|
||||
|
||||
### Enable the extension
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "vector" and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Example: enable the "vector" extension.
|
||||
create extension vector
|
||||
with
|
||||
schema extensions;
|
||||
|
||||
-- Example: disable the "vector" extension
|
||||
drop
|
||||
extension if exists vector;
|
||||
```
|
||||
|
||||
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
|
||||
To disable an extension, call `drop extension`.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Usage
|
||||
|
||||
### Create a table to store vectors
|
||||
|
||||
```sql
|
||||
create table
|
||||
posts (
|
||||
id serial primary key,
|
||||
title text not null,
|
||||
body text not null,
|
||||
embedding vector (1536)
|
||||
);
|
||||
```
|
||||
|
||||
### Storing a vector / embedding
|
||||
|
||||
In this example we'll generate a vector using the OpenAI API client, then store it in the database using the Supabase client.
|
||||
|
||||
```js
|
||||
const title = 'First post!'
|
||||
const body = 'Hello world!'
|
||||
|
||||
// Generate a vector using OpenAI
|
||||
const embeddingResponse = await openai.createEmbedding({
|
||||
model: 'text-embedding-ada-002',
|
||||
input: body,
|
||||
})
|
||||
const [responseData] = embeddingResponse.data.data.
|
||||
|
||||
// Store the vector in Postgres
|
||||
const { data, error } = await supabase.from('posts').insert({
|
||||
title,
|
||||
body,
|
||||
embedding: responseData.embedding,
|
||||
})
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Source code: [github.com/pgvector/pgvector](https://github.com/pgvector/pgvector/)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,163 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'rum',
|
||||
title: 'RUM: improved inverted index for full-text search based on GIN index',
|
||||
description: 'A GIN-like index with additional tree-organized data for each index entry',
|
||||
}
|
||||
|
||||
[RUM](https://github.com/postgrespro/rum) is an extension which adds a RUM index to Postgresql.
|
||||
|
||||
RUM index is based on GIN that stores additional per-entry information in a posting tree. For example, positional information of lexemes or timestamps. In comparison to GIN it can use this information to make faster index-only scans for:
|
||||
|
||||
- Phrase search
|
||||
- Text search with ranking by text distance operator
|
||||
- Text `SELECT`s with ordering by some non-indexed additional column e.g. by timestamp.
|
||||
|
||||
RUM works best in scenarios when the possible keys are highly repeatable. I.e. all texts are composed of a
|
||||
limited amount of words, so per-lexeme indexing gives significant speed-up in searching texts containing word
|
||||
combinations or phrases.
|
||||
|
||||
Main operators for ordering are:
|
||||
|
||||
tsvector `<=>` tsquery | float4 | Distance between tsvector and tsquery.
|
||||
value `<=>` value | float8 | Distance between two values.
|
||||
|
||||
Where value is timestamp, timestamptz, int2, int4, int8, float4, float8, money and oid
|
||||
|
||||
## Usage
|
||||
|
||||
### Enable the extension
|
||||
|
||||
You can get started with rum by enabling the extension in your Supabase dashboard.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "rum" and enable the extension.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Example: enable the "rum" extension
|
||||
create extension rum with schema extensions;
|
||||
|
||||
-- Example: disable the "rum" extension
|
||||
drop extension if exists rum;
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Syntax
|
||||
|
||||
#### For type: tsvector
|
||||
|
||||
To understand the following you may need first to see [Official PostgreSQL documentation on text
|
||||
search](https://www.postgresql.org/docs/current/functions-textsearch.html)
|
||||
|
||||
`rum_tsvector_ops`
|
||||
|
||||
```sql
|
||||
CREATE TABLE test_rum(t text, a tsvector);
|
||||
|
||||
CREATE TRIGGER tsvectorupdate
|
||||
BEFORE UPDATE OR INSERT ON test_rum
|
||||
FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('a', 'pg_catalog.english', 't');
|
||||
|
||||
INSERT INTO test_rum(t) VALUES ('The situation is most beautiful');
|
||||
INSERT INTO test_rum(t) VALUES ('It is a beautiful');
|
||||
INSERT INTO test_rum(t) VALUES ('It looks like a beautiful place');
|
||||
|
||||
CREATE INDEX rumidx ON test_rum USING rum (a rum_tsvector_ops);
|
||||
```
|
||||
|
||||
And we can execute `tsvector` selects with ordering by text distance operator:
|
||||
|
||||
```sql
|
||||
SELECT t, a `<=>` to_tsquery('english', 'beautiful | place') AS rank
|
||||
FROM test_rum
|
||||
WHERE a @@ to_tsquery('english', 'beautiful | place')
|
||||
ORDER BY a `<=>` to_tsquery('english', 'beautiful | place');
|
||||
t | rank
|
||||
---------------------------------+---------
|
||||
It looks like a beautiful place | 8.22467
|
||||
The situation is most beautiful | 16.4493
|
||||
It is a beautiful | 16.4493
|
||||
(3 rows)
|
||||
```
|
||||
|
||||
`rum_tsvector_addon_ops`
|
||||
|
||||
```sql
|
||||
CREATE TABLE tsts (id int, t tsvector, d timestamp);
|
||||
CREATE INDEX tsts_idx ON tsts USING rum (t rum_tsvector_addon_ops, d)
|
||||
WITH (attach = 'd', to = 't');
|
||||
```
|
||||
|
||||
Now we can execute the selects with ordering distance operator on attached column:
|
||||
|
||||
```sql
|
||||
SELECT id, d, d `<=>` '2016-05-16 14:21:25' FROM tsts WHERE t @@ 'wr&qh' ORDER BY d `<=>` '2016-05-16 14:21:25' LIMIT 5;
|
||||
id | d | ?column?
|
||||
-----+---------------------------------+---------------
|
||||
355 | Mon May 16 14:21:22.326724 2016 | 2.673276
|
||||
354 | Mon May 16 13:21:22.326724 2016 | 3602.673276
|
||||
371 | Tue May 17 06:21:22.326724 2016 | 57597.326724
|
||||
406 | Wed May 18 17:21:22.326724 2016 | 183597.326724
|
||||
415 | Thu May 19 02:21:22.326724 2016 | 215997.326724
|
||||
(5 rows)
|
||||
```
|
||||
|
||||
#### For type: anyarray
|
||||
|
||||
`rum_anyarray_ops`
|
||||
|
||||
This operator class stores anyarray elements with length of the array. It supports operators `&&`, `@>`, `<@`, `=`, `%` operators. It also supports ordering by `<=>` operator.
|
||||
|
||||
```sql
|
||||
CREATE TABLE test_array (i int2[]);
|
||||
INSERT INTO test_array VALUES ('{}'), ('{0}'), ('{1,2,3,4}'), ('{1,2,3}'), ('{1,2}'), ('{1}');
|
||||
CREATE INDEX idx_array ON test_array USING rum (i rum_anyarray_ops);
|
||||
```
|
||||
|
||||
Now we can execute the query using index scan:
|
||||
|
||||
```sql
|
||||
SELECT * FROM test_array WHERE i && '{1}' ORDER BY i `<=>` '{1}' ASC;
|
||||
i
|
||||
-----------
|
||||
{1}
|
||||
{1,2}
|
||||
{1,2,3}
|
||||
{1,2,3,4}
|
||||
(4 rows)
|
||||
```
|
||||
|
||||
`rum_anyarray_addon_ops`
|
||||
|
||||
The does the same with anyarray index as `rum_tsvector_addon_ops` i.e. allows to order select results using distance
|
||||
operator by attached column.
|
||||
|
||||
## Limitations
|
||||
|
||||
`RUM` has slower build and insert times than `GIN` due to:
|
||||
|
||||
1. It is bigger due to the additional attributes stored in the index.
|
||||
2. It uses generic WAL records.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Official RUM documentation](https://github.com/postgrespro/rum)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -34,7 +34,10 @@ You don't have to be a database expert to start using Supabase. Our table view m
|
||||
Dig into the relationships within your data.
|
||||
|
||||
<video width="99%" loop="" muted="" playsInline="" controls="true">
|
||||
<source src="/docs/videos/relational-drilldown-zoom.mp4" type="video/mp4" />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/relational-drilldown-zoom.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Clone tables
|
||||
@@ -42,7 +45,12 @@ Dig into the relationships within your data.
|
||||
You can duplicate your tables, just like you would inside a spreadsheet.
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source src="/docs/videos/duplicate-tables.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/duplicate-tables.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### The SQL Editor
|
||||
@@ -50,7 +58,12 @@ You can duplicate your tables, just like you would inside a spreadsheet.
|
||||
Supabase comes with a SQL Editor. You can also save your favorite queries to run later!
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source src="/docs/videos/favorites.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/favorites.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Additional features
|
||||
@@ -74,7 +87,12 @@ To expand the functionality of your Postgres database, you can use extensions.
|
||||
You can enable Postgres extensions with the click of a button within the Supabase dashboard.
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source src="/docs/videos/toggle-extensions.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/toggle-extensions.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
[Learn more](/docs/guides/database/extensions) about all the extensions provided on Supabase.
|
||||
|
||||
@@ -21,7 +21,12 @@ Replication is done through _publications_, a method of choosing which changes t
|
||||
4. Control which tables broadcast changes by selecting **Source** and toggling each table.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/api/api-realtime.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-realtime.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
## Create a publication
|
||||
|
||||
@@ -42,7 +42,12 @@ and run the SQL queries yourself.
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/api/api-create-table-sm.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
1. Go to the [Table Editor](https://app.supabase.com/project/_/editor) page in the Dashboard.
|
||||
|
||||
@@ -32,8 +32,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||
PROJECT_ID: zdtdtxajzydjqzuktnqx
|
||||
SUPABASE_ACCESS_TOKEN: YOUR_SUPABASE_ACCESS_TOKEN
|
||||
PROJECT_ID: YOUR_SUPABASE_PROJECT_ID
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -96,11 +96,11 @@ Receive your database changes through websockets. [Docs](/docs/guides/realtime/p
|
||||
|
||||
### User Broadcasting
|
||||
|
||||
Send messages between connected users through websockets. [Docs](/docs/guides/realtime#broadcast).
|
||||
Send messages between connected users through websockets. [Docs](/docs/guides/realtime/broadcast).
|
||||
|
||||
### User Presence
|
||||
|
||||
Synchronize shared state across your users, including online status and typing indicators. [Docs](/docs/guides/realtime#presence).
|
||||
Synchronize shared state across your users, including online status and typing indicators. [Docs](/docs/guides/realtime/presence).
|
||||
|
||||
### Client libraries
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ import {
|
||||
User,
|
||||
} from '@supabase/supabase-js'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { Database } from 'src/schema'
|
||||
|
||||
export interface Profile {
|
||||
id?: string
|
||||
@@ -136,7 +135,7 @@ Let's set up an Angular component to manage logins and sign ups. We'll use Magic
|
||||
Create an **AuthComponent** with `ng g c auth` Angular CLI command.
|
||||
|
||||
```ts title=src/app/auth/auth.component.ts
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component } from '@angular/core'
|
||||
import { FormBuilder } from '@angular/forms'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
|
||||
@@ -145,7 +144,7 @@ import { SupabaseService } from '../supabase.service'
|
||||
templateUrl: './auth.component.html',
|
||||
styleUrls: ['./auth.component.css'],
|
||||
})
|
||||
export class AuthComponent implements OnInit {
|
||||
export class AuthComponent {
|
||||
loading = false
|
||||
|
||||
signInForm = this.formBuilder.group({
|
||||
@@ -157,8 +156,6 @@ export class AuthComponent implements OnInit {
|
||||
private readonly formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
async onSubmit(): Promise<void> {
|
||||
try {
|
||||
this.loading = true
|
||||
@@ -406,7 +403,7 @@ Let's create an avatar for the user so that they can upload a profile photo.
|
||||
Create an **AvatarComponent** with `ng g c avatar` Angular CLI command.
|
||||
|
||||
```ts title=src/app/avatar/avatar.component.ts
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
|
||||
@@ -415,7 +412,7 @@ import { SupabaseService } from '../supabase.service'
|
||||
templateUrl: './avatar.component.html',
|
||||
styleUrls: ['./avatar.component.css'],
|
||||
})
|
||||
export class AvatarComponent implements OnInit {
|
||||
export class AvatarComponent {
|
||||
_avatarUrl: SafeResourceUrl | undefined
|
||||
uploading = false
|
||||
|
||||
@@ -430,8 +427,6 @@ export class AvatarComponent implements OnInit {
|
||||
|
||||
constructor(private readonly supabase: SupabaseService, private readonly dom: DomSanitizer) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
async downloadImage(path: string) {
|
||||
try {
|
||||
const { data } = await this.supabase.downLoadImage(path)
|
||||
|
||||
@@ -129,7 +129,12 @@ or you can just copy/paste the SQL from below and run it yourself.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/sql-user-management-starter.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabPanel>
|
||||
@@ -204,7 +209,12 @@ We just need to get the URL as well as the `anon`, `service_role` and `JWT_SECRE
|
||||
2. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
## Building the App
|
||||
|
||||
@@ -106,7 +106,7 @@ To tell Plasmic to look for your code components on your dev server, you’ll ne
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source
|
||||
src="/docs/videos/guides/integrations/plasmic/configure-app-host-plasmic-05.mp4"
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/plasmic/configure-app-host-plasmic-05.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
@@ -123,7 +123,7 @@ Create a new page called `Gallery`, and set a path for this page (`/gallery`).
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source
|
||||
src="/docs/videos/guides/integrations/plasmic/create-gallery-page-plasmic-06.mp4"
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/plasmic/create-gallery-page-plasmic-06.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
@@ -156,7 +156,7 @@ For your convenience, the following video shows you how to create the page end-t
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source
|
||||
src="/docs/videos/guides/integrations/plasmic/end-to-end-plasmic-10.mp4"
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/plasmic/end-to-end-plasmic-10.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
|
||||
@@ -92,12 +92,12 @@ This is what your `schema.prisma` file should look like:
|
||||
|
||||
```go
|
||||
datasource db {
|
||||
provider = “postgresql”
|
||||
url = env(“DATABASE_URL”)
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
generator client {
|
||||
provider = “prisma-client-js”
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
@@ -9,47 +9,17 @@ export const meta = {
|
||||
|
||||
Supabase provides a globally distributed cluster of [Realtime](https://github.com/supabase/realtime) servers that enable the following functionality:
|
||||
|
||||
- [Broadcast](#broadcast): Send ephemeral messages from client to clients with low latency.
|
||||
- [Presence](#presence): Track and synchronize shared state between clients.
|
||||
- [Postgres Changes](#postgres-changes): Listen to Postgres database changes and send them to authorized clients.
|
||||
- [Broadcast](/docs/guides/realtime/broadcast): Send ephemeral messages from client to clients with low latency.
|
||||
- [Presence](/docs/guides/realtime/presence): Track and synchronize shared state between clients.
|
||||
- [Postgres Changes](/docs/guides/realtime/postgres-changes): Listen to Postgres database changes and send them to authorized clients.
|
||||
|
||||
A [channel](https://hexdocs.pm/phoenix/channels.html) is the basic building block of Realtime and narrows the scope of data flow to subscribed clients. You can think of a channel as a chatroom where participants are able to see who's online and send and receive messages; similar to a Discord or Slack channel.
|
||||
|
||||
All clients can connect to a channel and take advantage of the built-in features, Broadcast and Presence, while extensions, like Postgres Changes, must be enabled prior to use.
|
||||
|
||||
## Broadcast
|
||||
|
||||
Broadcast follows the [publish-subscribe pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) where a client publishes messages to a channel with a unique identifier. For example, a user could send a message to a channel with id `room-1`.
|
||||
|
||||
Other clients can elect to receive the message in real-time by subscribing to the channel with id `room-1`. If these clients are online and subscribed then they will receive the message.
|
||||
|
||||
Broadcast works by connecting your client to the nearest Realtime server, which will communicate with other servers to relay messages to other clients.
|
||||
|
||||
A common use-case is sharing a user's cursor position with other clients in an online game.
|
||||
|
||||
## Presence
|
||||
|
||||
Presence utilizes an in-memory conflict-free replicated data type (CRDT) to track and synchronize shared state in an eventually consistent manner. It computes the difference between existing state and new state changes and sends the necessary updates to clients via Broadcast.
|
||||
|
||||
When a new client subscribes to a channel, it will immediately receive the channel's latest state in a single message instead of waiting for all other clients to send their individual states.
|
||||
|
||||
Clients are free to come-and-go as they please, and as long as they are all subscribed to the same channel then they will all have the same Presence state as each other.
|
||||
|
||||
The neat thing about Presence is that if a client is suddenly disconnected (for example, they go offline), their state will be automatically removed from the shared state. If you've ever tried to build an “I'm online” feature which handles unexpected disconnects, you'll appreciate how useful this is.
|
||||
|
||||
## Postgres Changes
|
||||
|
||||
Postgres Changes enable you to listen to database changes and have them broadcast to authorized clients based on [Row Level Security (RLS)](/docs/guides/auth/row-level-security) policies.
|
||||
|
||||
This works by Realtime polling your database's logical replication slot for changes, passing those changes to the [apply_rls](https://github.com/supabase/walrus#reading-wal) SQL function to determine which clients have permission, and then using Broadcast to send those changes to clients.
|
||||
|
||||
Realtime requires a publication called `supabase_realtime` to determine which tables to poll. You must add tables to this publication prior to clients subscribing to channels that want to listen for database changes.
|
||||
|
||||
We strongly encourage you to enable RLS on your database tables and have RLS policies in place to prevent unauthorized parties from accessing your data.
|
||||
|
||||
## See Also
|
||||
|
||||
- [Realtime quickstart](/docs/guides/realtime/quickstart)
|
||||
- [Quickstart](/docs/guides/realtime/quickstart)
|
||||
- [Realtime: Multiplayer Edition](https://supabase.com/blog/supabase-realtime-multiplayer-general-availability) blog post
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'broadcast',
|
||||
title: 'Broadcast',
|
||||
description: "Getting started with Realtime's Broadcast feature",
|
||||
}
|
||||
|
||||
Broadcast follows the [publish-subscribe pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) where a client publishes messages to a channel with a unique identifier. For example, a user could send a message to a channel with id `room-1`.
|
||||
|
||||
Other clients can elect to receive the message in real-time by subscribing to the channel with id `room-1`. If these clients are online and subscribed then they will receive the message.
|
||||
|
||||
Broadcast works by connecting your client to the nearest Realtime server, which will communicate with other servers to relay messages to other clients.
|
||||
|
||||
A common use-case is sharing a user's cursor position with other clients in an online game.
|
||||
|
||||
## Listen to Messages
|
||||
|
||||
You can get started with Broadcast by creating a client and listening to a channel's messages:
|
||||
|
||||
```js
|
||||
const { createClient } = require('@supabase/supabase-js')
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
||||
|
||||
supabase
|
||||
.channel('test')
|
||||
.on('broadcast', { event: 'supa' }, (payload) => console.log(payload))
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
## Send Messages
|
||||
|
||||
You can create another client and send messages to other clients:
|
||||
|
||||
```js
|
||||
const { createClient } = require('@supabase/supabase-js')
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
||||
|
||||
supabase.channel('test').subscribe((status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
channel.send({
|
||||
type: 'broadcast',
|
||||
event: 'supa',
|
||||
payload: { org: 'supabase' },
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
In order for clients to successfully send and receive mesages to one another, they must both specify the same `event`.
|
||||
|
||||
We recommend that the client has successfully subscribed to the channel prior to sending messages.
|
||||
|
||||
### Self-Send Messages
|
||||
|
||||
You can also choose for a client to receive messages that it sent:
|
||||
|
||||
```js
|
||||
// Supabase client setup
|
||||
|
||||
supabase
|
||||
.channel('test', {
|
||||
config: {
|
||||
broadcast: {
|
||||
self: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
.on('broadcast', { event: 'supa' }, (payload) => console.log(payload))
|
||||
.subscribe((status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
channel.send({
|
||||
type: 'broadcast',
|
||||
event: 'supa',
|
||||
payload: { org: 'supabase' },
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Acknowledge Messages
|
||||
|
||||
You can ensure that Realtime's servers received your message by:
|
||||
|
||||
```js
|
||||
// Supabase client setup
|
||||
|
||||
const channel = supabase.channel('receipt', {
|
||||
config: {
|
||||
broadcast: { ack: true },
|
||||
},
|
||||
})
|
||||
|
||||
channel.subscribe(async (status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
const resp = await channel.send({
|
||||
type: 'broadcast',
|
||||
event: 'latency',
|
||||
payload: {},
|
||||
})
|
||||
console.log(resp)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
If `ack` is not set to `true`, Realtime servers will not acknowledge that it received the sent message and `send` promise resolves immediately.
|
||||
|
||||
## Client-Side Rate Limit
|
||||
|
||||
There is a default client-side rate limit that enables you to send 10 messages per second, or one message every 100 milliseconds. You can customize this when creating the client:
|
||||
|
||||
```js
|
||||
const { createClient } = require('@supabase/supabase-js')
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.SUPABASE_URL,
|
||||
process.env.SUPABASE_KEY,
|
||||
{
|
||||
realtime: {
|
||||
params: {
|
||||
eventsPerSecond: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By setting `eventsPerSecond` to 20, you can send one message every 50 milliseconds on a per client basis.
|
||||
|
||||
Learn more by visiting the [Rate Limits](/docs/guides/realtime/rate-limits) section.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -19,11 +19,12 @@ GRANT SELECT ON "private_schema"."table" TO authenticated;
|
||||
```
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
We strongly encourage you to enable RLS and create policies for tables in private schemas. Otherwise, any role you grant access to will have unfettered read access to the table.
|
||||
|
||||
We strongly encourage you to enable RLS and create policies for tables in private schemas.
|
||||
Otherwise, any role you grant access to will have unfettered read access to the table.
|
||||
</Admonition>
|
||||
|
||||
## Replication Setup
|
||||
|
||||
You can do this in the [Replication](https://app.supabase.com/project/_/database/replication) section in the Dashboard or with the [SQL editor](https://app.supabase.com/project/_/sql):
|
||||
|
||||
```sql
|
||||
@@ -39,6 +40,8 @@ commit;
|
||||
alter publication supabase_realtime add table messages;
|
||||
```
|
||||
|
||||
### Full `old` Record
|
||||
|
||||
By default, only `new` record changes are sent but if you want to receive the `old` record (previous values) whenever you `UPDATE` or `DELETE` a record,
|
||||
you can set the `replica identity` of your table to `full`:
|
||||
|
||||
@@ -46,6 +49,8 @@ you can set the `replica identity` of your table to `full`:
|
||||
alter table messages replica identity full;
|
||||
```
|
||||
|
||||
## Schema Changes
|
||||
|
||||
To listen to all changes in the `public` schema:
|
||||
|
||||
```js
|
||||
@@ -74,6 +79,8 @@ const channel = supabase
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
## Table Changes
|
||||
|
||||
To listen to changes on a table in the `public` schema:
|
||||
|
||||
```js
|
||||
@@ -93,6 +100,8 @@ const channel = supabase
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
## Filter Changes
|
||||
|
||||
To listen to changes when a column's value in a table matches a client-specified value:
|
||||
|
||||
```js
|
||||
@@ -113,6 +122,8 @@ const channel = supabase
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
## Combination Changes
|
||||
|
||||
To listen to different events and schema/tables/filters combinations with the same channel:
|
||||
|
||||
```js
|
||||
@@ -130,7 +141,8 @@ const channel = supabase
|
||||
},
|
||||
(payload) => console.log(payload)
|
||||
)
|
||||
.on('postgres_changes',
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
@@ -141,6 +153,38 @@ const channel = supabase
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
## Custom Tokens
|
||||
|
||||
You may choose to sign your own tokens to customize claims that can be checked in your RLS policies.
|
||||
In order for this to work you must pass `apikey` in both Realtime's `headers` and `params` when creating the client.
|
||||
|
||||
The `apikey` in `headers` must be either the `anon` or `service_role` token that Supabase signed.
|
||||
This will authenticate your request in the API gateway.
|
||||
|
||||
<Admonition type="caution">
|
||||
Do not expose the `service_role` token on the client because the role is authorized to bypass
|
||||
row-level security.
|
||||
</Admonition>
|
||||
|
||||
The `apikey` in `params` can be your custom token signed with the JWT secret of your Supabase project.
|
||||
This is forwarded to the Realtime server and it will verify your custom token and use its claims to authorize database changes
|
||||
when RLS is enabled.
|
||||
|
||||
```js
|
||||
const { createClient } = require('@supabase/supabase-js')
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY, {
|
||||
realtime: {
|
||||
headers: {
|
||||
apikey: 'supabase-signed-token',
|
||||
},
|
||||
params: {
|
||||
apikey: 'your-custom-token',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,165 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'presence',
|
||||
title: 'Presence',
|
||||
description: "Getting started with Realtime's Presence feature",
|
||||
}
|
||||
|
||||
Presence utilizes an in-memory conflict-free replicated data type (CRDT) to track and synchronize shared state in an eventually consistent manner. It computes the difference between existing state and new state changes and sends the necessary updates to clients via Broadcast.
|
||||
|
||||
When a new client subscribes to a channel, it will immediately receive the channel's latest state in a single message instead of waiting for all other clients to send their individual states.
|
||||
|
||||
Clients are free to come-and-go as they please, and as long as they are all subscribed to the same channel then they will all have the same Presence state as each other.
|
||||
|
||||
The neat thing about Presence is that if a client is suddenly disconnected (for example, they go offline), their state will be automatically removed from the shared state. If you've ever tried to build an “I'm online” feature which handles unexpected disconnects, you'll appreciate how useful this is.
|
||||
|
||||
## Presence State
|
||||
|
||||
You can get started by listening to `sync` event messages notifying the client that a channel's state has been synchronized on the server. You can get the state by calling the channel's `presenceState` helper:
|
||||
|
||||
```js
|
||||
const { createClient } = require('@supabase/supabase-js')
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
||||
|
||||
const channel = supabase.channel('test')
|
||||
|
||||
channel
|
||||
.on('presence', { event: 'sync' }, () => {
|
||||
const state = channel.presenceState()
|
||||
console.log(state)
|
||||
})
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
Whenever there's Presence activity on the `'test'` channel, this `sync` event will be broadcast to all clients subscribed to the channel.
|
||||
|
||||
## Listen to Joins
|
||||
|
||||
You can create a client and listen to new state joining the channel's Presence:
|
||||
|
||||
```js
|
||||
// Supabase client setup
|
||||
|
||||
const channel = supabase.channel('test')
|
||||
|
||||
channel
|
||||
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
|
||||
console.log(key, newPresences)
|
||||
})
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
## Track Presence
|
||||
|
||||
On another client, subscribe to the channel and insert state to be tracked by Presence:
|
||||
|
||||
```js
|
||||
// Supabase client setup
|
||||
|
||||
const channel = supabase.channel('test')
|
||||
|
||||
channel.subscribe(async (status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
const presenceTrackStatus = await channel.track({
|
||||
user: 'user-1',
|
||||
online_at: new Date().toISOString(),
|
||||
})
|
||||
console.log(presenceTrackStatus)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Presence Key
|
||||
|
||||
By default, Presence will generate an `UUIDv1` key on the server to uniquely track a client channel's state but you may pass Presence a custom key when creating the channel.
|
||||
|
||||
```js
|
||||
// Supabase client setup
|
||||
|
||||
const channel = supabase.channel('test', {
|
||||
config: {
|
||||
presence: {
|
||||
key: 'userId-1',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
channel.subscribe(async (status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
const presenceTrackStatus = await channel.track({
|
||||
user: 'user-1',
|
||||
online_at: new Date().toISOString(),
|
||||
})
|
||||
console.log(presenceTrackStatus)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Listen to Leaves
|
||||
|
||||
You can create a client and listen to a client channel's state leaving:
|
||||
|
||||
```js
|
||||
// Supabase client setup
|
||||
|
||||
const channel = supabase.channel('test')
|
||||
|
||||
channel
|
||||
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
|
||||
console.log(key, leftPresences)
|
||||
})
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
## Untrack Presence
|
||||
|
||||
On another client, subscribe to the channel, and remove tracked state from Presence:
|
||||
|
||||
```js
|
||||
// Supabase client setup
|
||||
|
||||
const channel = supabase.channel('test')
|
||||
|
||||
channel.subscribe(async (status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
const presenceTrackStatus = await channel.track({
|
||||
user: 'user-1',
|
||||
online_at: new Date().toISOString(),
|
||||
})
|
||||
|
||||
if (presenceTrackStatus === 'ok') {
|
||||
const presenceUntrackStatus = await channel.untrack()
|
||||
console.log(presenceUntrackStatus)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Client-Side Rate Limit
|
||||
|
||||
There is a default client-side rate limit that enables you to send 10 messages per second, or one message every 100 milliseconds. You can customize this when creating the client:
|
||||
|
||||
```js
|
||||
const { createClient } = require('@supabase/supabase-js')
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.SUPABASE_URL,
|
||||
process.env.SUPABASE_KEY,
|
||||
{
|
||||
realtime: {
|
||||
params: {
|
||||
eventsPerSecond: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By setting `eventsPerSecond` to 5, you can send one message every 200 milliseconds on a per client basis.
|
||||
|
||||
Learn more by visiting the [Rate Limits](/docs/guides/realtime/rate-limits) section.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -27,7 +27,7 @@ npm install @supabase/supabase-js
|
||||
|
||||
## Cursor Positions
|
||||
|
||||
[Broadcast](/docs/guides/realtime#broadcast) allows a client to send messages and multiple clients to receive the messages. The broadcasted messages are ephemeral. They are not persisted to the database and are directly relayed through the Realtime servers. This is ideal for sending information like cursor positions where minimal latency is important, but persisting them is not.
|
||||
[Broadcast](/docs/guides/realtime/broadcast) allows a client to send messages and multiple clients to receive the messages. The broadcasted messages are ephemeral. They are not persisted to the database and are directly relayed through the Realtime servers. This is ideal for sending information like cursor positions where minimal latency is important, but persisting them is not.
|
||||
|
||||
In [multiplayer.dev](https://multiplayer.dev), client's cursor positions are sent to other clients in the room. However, cursor positions will be randomly generated for this example.
|
||||
|
||||
@@ -124,7 +124,7 @@ channel.subscribe(async (status) => {
|
||||
|
||||
## Track and Display Which Users Are Online
|
||||
|
||||
[Presence](/docs/guides/realtime#presence) stores and synchronize shared state across clients. The `sync` event is triggered whenever the shared state changes. The `join` event is triggered when new clients join the channel and `leave` event is triggered when clients leave.
|
||||
[Presence](/docs/guides/realtime/presence) stores and synchronize shared state across clients. The `sync` event is triggered whenever the shared state changes. The `join` event is triggered when new clients join the channel and `leave` event is triggered when clients leave.
|
||||
|
||||
Each client can use the channel's `track` method to store an object in shared state. Each client can only track one object, and if `track` is called again by the same client, then the new object overwrites the previously tracked object in the shared state. You can use one client to track and display users who are online:
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@ import Link from 'next/link'
|
||||
import { GlassPanel, IconPanel, Button, IconChevronRight } from 'ui'
|
||||
|
||||
export const meta = {
|
||||
id: 'resources',
|
||||
title: 'Resources',
|
||||
description: 'Resources for getting started building with Supabase.',
|
||||
sidebar_label: 'Overview',
|
||||
}
|
||||
|
||||
<div className="flex flex-col gap-12 my-12">
|
||||
@@ -64,18 +62,46 @@ export const meta = {
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="max-w-xl mb-6">
|
||||
|
||||
### Postgres Resources
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-12 gap-6 not-prose">
|
||||
{postgres.map((resource) => {
|
||||
return (
|
||||
<Link
|
||||
href={`${resource.href}`}
|
||||
key={resource.title}
|
||||
passHref
|
||||
>
|
||||
<a className={'col-span-12 md:col-span-4'}>
|
||||
<GlassPanel {...resource} background={false} showIconBg={true}>
|
||||
{resource.description}
|
||||
</GlassPanel>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
|
||||
})}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* end of container */}
|
||||
|
||||
</div>
|
||||
|
||||
export const resources = [
|
||||
// removing until the examples page is reworked
|
||||
// {
|
||||
// title: 'Examples',
|
||||
// hasLightIcon: true,
|
||||
// href: '/guides/resources/examples',
|
||||
// description: 'Official GitHub examples, curated content from the community, and more.',
|
||||
// },
|
||||
{
|
||||
title: 'Examples',
|
||||
hasLightIcon: true,
|
||||
href: '/guides/resources/examples',
|
||||
description: 'Official GitHub examples, curated content from the community, and more.',
|
||||
},
|
||||
{
|
||||
title: 'Glossary',
|
||||
hasLightIcon: true,
|
||||
@@ -111,6 +137,27 @@ export const migrationGuides = [
|
||||
},
|
||||
]
|
||||
|
||||
export const postgres = [
|
||||
{
|
||||
title: 'Drop all tables in schema',
|
||||
hasLightIcon: true,
|
||||
href: '/guides/resources/postgres/dropping-all-tables-in-schema',
|
||||
description: 'Delete all tables in a given schema.',
|
||||
},
|
||||
{
|
||||
title: 'Select first row per group',
|
||||
hasLightIcon: true,
|
||||
href: '/guides/resources/postgres/first-row-in-group',
|
||||
description: 'Retrieve the first row in each distinct group.',
|
||||
},
|
||||
{
|
||||
title: 'Print PostgreSQL version',
|
||||
hasLightIcon: true,
|
||||
href: '/guides/resources/postgres/which-version-of-postgres',
|
||||
description: 'Find out which version of Postgres you are running.',
|
||||
},
|
||||
]
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
File renamed without changes.
@@ -0,0 +1,34 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Drop all tables in a PostgreSQL schema',
|
||||
description: 'Useful snippet for deleting all tables in a given schema',
|
||||
footerHelpType: 'postgres',
|
||||
}
|
||||
|
||||
Execute the following query to drop all tables in a given schema.
|
||||
Replace `my-schema-name` with the name of your schema. In Supabase, the default schema is `public`.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
This deletes all tables and their associated data. Ensure you have a recent [backup](/docs/guides/platform/backups) before proceeding.
|
||||
|
||||
</Admonition>
|
||||
|
||||
```sql
|
||||
do $$ declare
|
||||
r record;
|
||||
begin
|
||||
for r in (select tablename from pg_tables where schemaname = 'my-schema-name') loop
|
||||
execute 'drop table if exists ' || quote_ident(r.tablename) || ' cascade';
|
||||
end loop;
|
||||
end $$;
|
||||
```
|
||||
|
||||
This query works by listing out all the tables in the given schema and then executing a `drop table` for each (hence the `for... loop`).
|
||||
|
||||
You can run this query using the [SQL Editor](https://app.supabase.com/project/_/sql) in the Supabase Dashboard, or via `psql` if you're [connecting directly to the database](/docs/guides/database/connecting-to-postgres#direct-connections).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,52 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Select first row for each group in PostgreSQL',
|
||||
description: 'PostgreSQL snippet for grabbing the first row in each distinct group by group',
|
||||
footerHelpType: 'postgres',
|
||||
}
|
||||
|
||||
Given a table `seasons`:
|
||||
|
||||
| id | team | points |
|
||||
| --- | :-------: | -----: |
|
||||
| 1 | Liverpool | 82 |
|
||||
| 2 | Liverpool | 84 |
|
||||
| 3 | Brighton | 34 |
|
||||
| 4 | Brighton | 28 |
|
||||
| 5 | Liverpool | 79 |
|
||||
|
||||
We want to find the rows containing the maximum number of points _per team_.
|
||||
|
||||
The expected output we want is:
|
||||
|
||||
| id | team | points |
|
||||
| --- | :-------: | -----: |
|
||||
| 3 | Brighton | 34 |
|
||||
| 2 | Liverpool | 84 |
|
||||
|
||||
From the [SQL Editor](https://app.supabase.com/project/_/sql), you can run a query like:
|
||||
|
||||
```sql
|
||||
select distinct
|
||||
on (team) id,
|
||||
team,
|
||||
points
|
||||
from
|
||||
seasons
|
||||
order BY
|
||||
id,
|
||||
points desc,
|
||||
team;
|
||||
```
|
||||
|
||||
The important bits here are:
|
||||
|
||||
- The `desc` keyword to order the `points` from highest to lowest.
|
||||
- The `distinct` keyword that tells Postgres to only return a single row per team.
|
||||
|
||||
This query can also be executed via `psql` or any other query editor if you prefer to [connect directly to the database](/docs/guides/database/connecting-to-postgres#direct-connections).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,28 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Print PostgreSQL version',
|
||||
description: 'Useful snippet for finding out which version of postgres you are running',
|
||||
footerHelpType: 'postgres',
|
||||
}
|
||||
|
||||
It's important to know which version of PostgreSQL you are running as each major version has different features and may cause breaking changes. You may also need to update your schema when [upgrading](https://www.postgresql.org/docs/current/pgupgrade.html) or downgrading to a major Postgres version.
|
||||
|
||||
Run the following query using the [SQL Editor](https://app.supabase.com/project/_/sql) in the Supabase Dashboard:
|
||||
|
||||
```sql
|
||||
select
|
||||
version ();
|
||||
```
|
||||
|
||||
Which should return something like:
|
||||
|
||||
```sql
|
||||
PostgreSQL 15.1 on aarch64-unknown-linux-gnu, compiled by gcc (Ubuntu 10.3.0-1ubuntu1~20.04) 10.3.0, 64-bit
|
||||
```
|
||||
|
||||
This query can also be executed via `psql` or any other query editor if you prefer to [connect directly to the database](/docs/guides/database/connecting-to-postgres#direct-connections).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -104,10 +104,8 @@ To get started, create a `supabase-image-loader.js` file in your NextJS project
|
||||
```ts
|
||||
const projectId = '' // your supabase project id
|
||||
|
||||
export default function supabaseLoader({ src, width, height, quality }) {
|
||||
return `https://${projectId}.supabase.co/storage/v1/render/image/public/${src}?width=${width}&height=${
|
||||
height || 0
|
||||
}&quality=${quality || 75}`
|
||||
export default function supabaseLoader({ src, width, quality }) {
|
||||
return `https://${projectId}.supabase.co/storage/v1/render/image/public/${src}?width=${width}&quality=${quality || 75}`
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ const tocList = TAGS.map((tag) =>
|
||||
specFile.parameters
|
||||
.filter((param: Parameter) => param.tags[0] === tag)
|
||||
.map((parameter) => {
|
||||
console.log('the param', parameter)
|
||||
const text = parameter.id
|
||||
const link = `#${parameter.id}`
|
||||
const level = '2'
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 313 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 980 KiB |
+377
-263
@@ -6,12 +6,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/getting-started</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/index</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -30,6 +24,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/cli</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -91,7 +91,7 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/tutorials</loc>
|
||||
<loc>https://supabase.com/docs/reference/index</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -108,24 +108,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/new/auth</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/tutorials/nextjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/api/generating-types</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -204,6 +186,18 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/cli/local-development</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/cli/managing-environments</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/arrays</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -295,13 +289,19 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/auth</loc>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/architecture</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/best-practices</loc>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/features</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -312,6 +312,30 @@
|
||||
<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>
|
||||
@@ -319,13 +343,13 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/architecture</loc>
|
||||
<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/getting-started/features</loc>
|
||||
<loc>https://supabase.com/docs/guides/functions/secrets</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -390,6 +414,18 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/integrations/flutterflow</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/integrations/illa</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/integrations/integrations</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -414,6 +450,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/integrations/picket</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/integrations/plasmic</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -510,6 +552,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/http-status-codes</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/logs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -552,6 +600,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/troubleshooting</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/postgres-changes</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -577,7 +631,7 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/supabase-cli</loc>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/docker</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -612,12 +666,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/docker</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>
|
||||
@@ -648,6 +696,180 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/http</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgcron</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgnet</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgtap</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/plv8</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/postgis</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/rum</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/uuid-ossp</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/flutter</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/nextjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/nuxtjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/reactjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/solidjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/sveltekit</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/vue</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/auth/auth-helpers/auth-ui</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -696,6 +918,108 @@
|
||||
<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/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/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/resources/migrating-to-supabase/firebase-auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/migrating-to-supabase/firebase-storage</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/migrating-to-supabase/firestore-data</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/migrating-to-supabase/heroku</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/realtime/config</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/self-hosting/auth/config</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/storage/config</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>
|
||||
@@ -798,228 +1122,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/http</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgcron</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgnet</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgtap</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/plv8</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/postgis</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/uuid-ossp</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/functions/examples/og-image</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/nextjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/nuxtjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/reactjs</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/resources/supabase-cli/local-development</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/supabase-cli/managing-environments</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/auth/config</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>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/migrating-to-supabase/firebase-storage</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/migrating-to-supabase/firestore-data</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/migrating-to-supabase/heroku</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/realtime/config</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/storage/config</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/javascript/initializing</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -1443,7 +1545,7 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/javascript/auth-admin-invoke</loc>
|
||||
<loc>https://supabase.com/docs/reference/javascript/functions-invoke</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -1677,7 +1779,7 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/javascript/v1/auth-admin-invoke</loc>
|
||||
<loc>https://supabase.com/docs/reference/javascript/v1/functions-invoke</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -2018,6 +2120,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/dart/initializing</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/dart/auth-signup</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -2085,7 +2193,7 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/dart/auth-admin-invoke</loc>
|
||||
<loc>https://supabase.com/docs/reference/dart/functions-invoke</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -2493,7 +2601,7 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/dart/v0/auth-admin-invoke</loc>
|
||||
<loc>https://supabase.com/docs/reference/dart/v0/functions-invoke</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -2895,7 +3003,7 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/dart/v0/auth-admin-invoke</loc>
|
||||
<loc>https://supabase.com/docs/reference/dart/v0/functions-invoke</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -3446,6 +3554,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-functions-download</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/cli/supabase-functions-deploy</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { createHash } from 'crypto'
|
||||
import dotenv from 'dotenv'
|
||||
import { ObjectExpression } from 'estree'
|
||||
import { readdir, readFile, stat } from 'fs/promises'
|
||||
import { Content, Root } from 'mdast'
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
import { mdxFromMarkdown, MdxjsEsm } from 'mdast-util-mdx'
|
||||
import { toMarkdown } from 'mdast-util-to-markdown'
|
||||
import { mdxjs } from 'micromark-extension-mdxjs'
|
||||
import 'openai'
|
||||
import { Configuration, OpenAIApi } from 'openai'
|
||||
import { join } from 'path'
|
||||
import { u } from 'unist-builder'
|
||||
import { filter } from 'unist-util-filter'
|
||||
import { inspect } from 'util'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const ignoredFiles = ['pages/404.mdx']
|
||||
|
||||
/**
|
||||
* Extracts ES literals from an `estree` `ObjectExpression`
|
||||
* into a plain JavaScript object.
|
||||
*/
|
||||
function getObjectFromExpression(node: ObjectExpression) {
|
||||
return node.properties.reduce<
|
||||
Record<string, string | number | bigint | true | RegExp | undefined>
|
||||
>((object, property) => {
|
||||
if (property.type !== 'Property') {
|
||||
return object
|
||||
}
|
||||
|
||||
const key = (property.key.type === 'Identifier' && property.key.name) || undefined
|
||||
const value = (property.value.type === 'Literal' && property.value.value) || undefined
|
||||
|
||||
if (!key) {
|
||||
return object
|
||||
}
|
||||
|
||||
return {
|
||||
...object,
|
||||
[key]: value,
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the `meta` ESM export from the MDX file.
|
||||
*
|
||||
* This info is akin to frontmatter.
|
||||
*/
|
||||
function extractMetaExport(mdxTree: Root) {
|
||||
const metaExportNode = mdxTree.children.find((node): node is MdxjsEsm => {
|
||||
return (
|
||||
node.type === 'mdxjsEsm' &&
|
||||
node.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' &&
|
||||
node.data.estree.body[0].declaration?.type === 'VariableDeclaration' &&
|
||||
node.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' &&
|
||||
node.data.estree.body[0].declaration.declarations[0].id.name === 'meta'
|
||||
)
|
||||
})
|
||||
|
||||
if (!metaExportNode) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const objectExpression =
|
||||
(metaExportNode.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' &&
|
||||
metaExportNode.data.estree.body[0].declaration?.type === 'VariableDeclaration' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0].id.name === 'meta' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0].init?.type ===
|
||||
'ObjectExpression' &&
|
||||
metaExportNode.data.estree.body[0].declaration.declarations[0].init) ||
|
||||
undefined
|
||||
|
||||
if (!objectExpression) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return getObjectFromExpression(objectExpression)
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a `mdast` tree into multiple trees based on
|
||||
* a predicate function. Will include the splitting node
|
||||
* at the beginning of each tree.
|
||||
*
|
||||
* Useful to split a markdown file into smaller sections.
|
||||
*/
|
||||
function splitTreeBy(tree: Root, predicate: (node: Content) => boolean) {
|
||||
return tree.children.reduce<Root[]>((trees, node) => {
|
||||
const [lastTree] = trees.slice(-1)
|
||||
|
||||
if (!lastTree || predicate(node)) {
|
||||
const tree: Root = u('root', [node])
|
||||
return trees.concat(tree)
|
||||
}
|
||||
|
||||
lastTree.children.push(node)
|
||||
return trees
|
||||
}, [])
|
||||
}
|
||||
|
||||
type Meta = ReturnType<typeof extractMetaExport>
|
||||
|
||||
type ProcessedMdx = {
|
||||
checksum: string
|
||||
meta: Meta
|
||||
sections: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes MDX content for search indexing.
|
||||
* It extracts metadata, strips it of all JSX,
|
||||
* and splits it into sub-sections based on criteria.
|
||||
*/
|
||||
function processMdxForSearch(content: string): ProcessedMdx {
|
||||
const checksum = createHash('sha256').update(content).digest('base64')
|
||||
|
||||
const mdxTree = fromMarkdown(content, {
|
||||
extensions: [mdxjs()],
|
||||
mdastExtensions: [mdxFromMarkdown()],
|
||||
})
|
||||
|
||||
const meta = extractMetaExport(mdxTree)
|
||||
|
||||
// Remove all MDX elements from markdown
|
||||
const mdTree = filter(
|
||||
mdxTree,
|
||||
(node) =>
|
||||
![
|
||||
'mdxjsEsm',
|
||||
'mdxJsxFlowElement',
|
||||
'mdxJsxTextElement',
|
||||
'mdxFlowExpression',
|
||||
'mdxTextExpression',
|
||||
].includes(node.type)
|
||||
)
|
||||
|
||||
if (!mdTree) {
|
||||
return {
|
||||
checksum,
|
||||
meta,
|
||||
sections: [],
|
||||
}
|
||||
}
|
||||
|
||||
const sectionTrees = splitTreeBy(mdTree, (node) => node.type === 'heading')
|
||||
|
||||
const sections = sectionTrees.map((tree) => toMarkdown(tree))
|
||||
|
||||
return {
|
||||
checksum,
|
||||
meta,
|
||||
sections,
|
||||
}
|
||||
}
|
||||
|
||||
async function walk(dir: string): Promise<string[]> {
|
||||
const immediateFiles = await readdir(dir)
|
||||
|
||||
const recursiveFiles = await Promise.all(
|
||||
immediateFiles.map(async (file) => {
|
||||
const filePath = join(dir, file)
|
||||
const stats = await stat(filePath)
|
||||
if (stats.isDirectory()) {
|
||||
return walk(filePath)
|
||||
} else if (stats.isFile()) {
|
||||
return [filePath]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const flattenedFiles = recursiveFiles.reduce(
|
||||
(all, folderContents) => all.concat(folderContents),
|
||||
[]
|
||||
)
|
||||
|
||||
return flattenedFiles
|
||||
}
|
||||
|
||||
async function generateEmbeddings() {
|
||||
if (
|
||||
!process.env.NEXT_PUBLIC_SUPABASE_URL ||
|
||||
!process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
||||
!process.env.OPENAI_KEY
|
||||
) {
|
||||
return console.log(
|
||||
'Environment variables NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, and OPENAI_KEY are required: skipping embeddings generation'
|
||||
)
|
||||
}
|
||||
|
||||
const supabaseClient = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
)
|
||||
|
||||
const markdownFiles = (await walk('pages'))
|
||||
.filter((fileName) => /\.mdx?$/.test(fileName))
|
||||
.filter((fileName) => !ignoredFiles.includes(fileName))
|
||||
|
||||
console.log(`Discovered ${markdownFiles.length} pages`)
|
||||
console.log('Checking which pages are new or have changed')
|
||||
|
||||
for (const markdownFile of markdownFiles) {
|
||||
const path = markdownFile.replace(/^pages/, '').replace(/\.mdx?$/, '')
|
||||
try {
|
||||
const contents = await readFile(markdownFile, 'utf8')
|
||||
|
||||
const { checksum, meta, sections } = processMdxForSearch(contents)
|
||||
|
||||
// Check for existing page in DB and compare checksums
|
||||
const { error: fetchPageError, data: existingPage } = await supabaseClient
|
||||
.from('page')
|
||||
.select()
|
||||
.filter('path', 'eq', path)
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
|
||||
if (fetchPageError) {
|
||||
throw fetchPageError
|
||||
}
|
||||
|
||||
// We use checksum to determine if this page & its sections need to be regenerated
|
||||
if (existingPage?.checksum === checksum) {
|
||||
continue
|
||||
}
|
||||
|
||||
console.log({ checksum, path, meta })
|
||||
|
||||
if (existingPage) {
|
||||
console.log(
|
||||
`Docs have changed for '${path}', removing old page sections and their embeddings`
|
||||
)
|
||||
|
||||
const { error: deletePageSectionError } = await supabaseClient
|
||||
.from('page_section')
|
||||
.delete()
|
||||
.filter('page_id', 'eq', existingPage.id)
|
||||
|
||||
if (deletePageSectionError) {
|
||||
throw deletePageSectionError
|
||||
}
|
||||
}
|
||||
|
||||
// Create/update page record. Intentionally clear checksum until we
|
||||
// have successfully generated all page sections.
|
||||
const { error: upsertPageError, data: page } = await supabaseClient
|
||||
.from('page')
|
||||
.upsert({ checksum: null, path, meta }, { onConflict: 'path' })
|
||||
.select()
|
||||
.limit(1)
|
||||
.single()
|
||||
|
||||
if (upsertPageError) {
|
||||
throw upsertPageError
|
||||
}
|
||||
|
||||
console.log(`Adding ${sections.length} page sections (with embeddings) for '${path}'`)
|
||||
for (const section of sections) {
|
||||
// OpenAI recommends replacing newlines with spaces for best results (specific to embeddings)
|
||||
const input = section.replace(/\n/g, ' ')
|
||||
|
||||
try {
|
||||
const configuration = new Configuration({ apiKey: process.env.OPENAI_KEY })
|
||||
const openai = new OpenAIApi(configuration)
|
||||
|
||||
const embeddingResponse = await openai.createEmbedding({
|
||||
model: 'text-embedding-ada-002',
|
||||
input,
|
||||
})
|
||||
|
||||
if (embeddingResponse.status !== 200) {
|
||||
throw new Error(inspect(embeddingResponse.data, false, 2))
|
||||
}
|
||||
|
||||
const [responseData] = embeddingResponse.data.data
|
||||
|
||||
const { error: insertPageSectionError, data: pageSection } = await supabaseClient
|
||||
.from('page_section')
|
||||
.insert({
|
||||
page_id: page.id,
|
||||
content: section,
|
||||
token_count: embeddingResponse.data.usage.total_tokens,
|
||||
embedding: responseData.embedding,
|
||||
})
|
||||
.select()
|
||||
.limit(1)
|
||||
.single()
|
||||
|
||||
if (insertPageSectionError) {
|
||||
throw insertPageSectionError
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO: decide how to better handle failed embeddings
|
||||
console.error(
|
||||
`Failed to generate embeddings for '${path}' page section starting with '${input.slice(
|
||||
0,
|
||||
40
|
||||
)}...'`
|
||||
)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Set page checksum so that we know this page was stored successfully
|
||||
const { error: updatePageError } = await supabaseClient
|
||||
.from('page')
|
||||
.update({ checksum })
|
||||
.filter('id', 'eq', page.id)
|
||||
|
||||
if (updatePageError) {
|
||||
throw updatePageError
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Page '${path}' or one/multiple of its page sections failed to store properly. Page has been marked with null checksum to indicate that it needs to be re-generated.`
|
||||
)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Embedding generation complete')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await generateEmbeddings()
|
||||
}
|
||||
|
||||
main().catch((err) => console.error(err))
|
||||
@@ -953,6 +953,7 @@ body {
|
||||
// search bar
|
||||
input[type='search'].DocSearch-Input {
|
||||
background-color: var(--colors-scale1);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark .DocSearch-Input {
|
||||
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
declare module 'sse.js' {
|
||||
export type SSEOptions = EventSourceInit & {
|
||||
headers?: Record<string, string>
|
||||
payload?: string
|
||||
method?: string
|
||||
}
|
||||
|
||||
export class SSE extends EventSource {
|
||||
constructor(url: string | URL, sseOptions?: SSEOptions)
|
||||
stream(): void
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,10 @@ We've made some massive improvements to our Table Editor that we're excited to s
|
||||
Last month we made it easy to drill into your table relationships. This month, we make it possible to drill multiple levels deep.
|
||||
|
||||
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls="true">
|
||||
<source src="/videos/relational-drilldown-zoom.mp4" type="video/mp4" />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/relational-drilldown-zoom.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
|
||||
#### Add, delete, and download rows
|
||||
@@ -49,7 +52,10 @@ Last month we made it easy to drill into your table relationships. This month, w
|
||||
We're making it easier to manipulate your data. Next month, you'll be able to add and remove columns directly from the Table view.
|
||||
|
||||
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls="true">
|
||||
<source src="/videos/csv-download-zoom.mp4" type="video/mp4" />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/csv-download-zoom.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
|
||||
### New Postgres Extensions
|
||||
|
||||
@@ -37,7 +37,13 @@ Supabase Auth provides all the backend services you need to authenticate and aut
|
||||
Supabase makes it simple to onboard your users with our new `supabase.auth.signUp()` and `supabase.auth.signIn()` [functions](/docs/guides/auth).
|
||||
|
||||
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
|
||||
<source src="/docs/videos/auth-zoom2.mp4" type="video/mp4" loop muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/auth-zoom2.mp4"
|
||||
type="video/mp4"
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Row Level Security
|
||||
@@ -45,7 +51,13 @@ Supabase makes it simple to onboard your users with our new `supabase.auth.signU
|
||||
Authentication only gets you so far. When you need granular authorization rules, nothing beats PostgreSQL's [Row Level Security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html). Supabase makes it simple to turn RLS on and off.
|
||||
|
||||
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
|
||||
<source src="/docs/videos/rls-zoom2.mp4" type="video/mp4" loop muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/rls-zoom2.mp4"
|
||||
type="video/mp4"
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Policies
|
||||
@@ -53,7 +65,13 @@ Authentication only gets you so far. When you need granular authorization rules,
|
||||
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are PostgreSQL's rule engine. They are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.
|
||||
|
||||
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
|
||||
<source src="/docs/videos/policies-zoom2.mp4" type="video/mp4" loop muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/policies-zoom2.mp4"
|
||||
type="video/mp4"
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
With policies, your database becomes the rules engine. Instead of repetitively filtering your queries, like this ...
|
||||
|
||||
@@ -31,7 +31,13 @@ Watch a full demo:
|
||||
Set up tables and columns directly from the table view.
|
||||
|
||||
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
|
||||
<source src="/videos/new-tables.mp4" type="video/mp4" loop muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/new-tables.mp4"
|
||||
type="video/mp4"
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Invite your team
|
||||
@@ -39,7 +45,13 @@ Set up tables and columns directly from the table view.
|
||||
You can now invite team members to your organisation.
|
||||
|
||||
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
|
||||
<source src="/videos/invite-team.mp4" type="video/mp4" loop muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/invite-team.mp4"
|
||||
type="video/mp4"
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Auth: Email Confirmations
|
||||
@@ -47,7 +59,13 @@ You can now invite team members to your organisation.
|
||||
You can now enable Email Confirmations for new users. This can be toggled on or off and the template for this email can be edited via the dashboard.
|
||||
|
||||
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
|
||||
<source src="/videos/confirm-email.mp4" type="video/mp4" loop muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/confirm-email.mp4"
|
||||
type="video/mp4"
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### TypeScript support
|
||||
|
||||
@@ -38,7 +38,12 @@ We've released OAuth logins! You can now enable third-party logins on your app f
|
||||
You can duplicate your tables, just like you would inside a spreadsheet.
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source src="/videos/duplicate-tables.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/duplicate-tables.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Enable and disable extensions
|
||||
@@ -46,7 +51,12 @@ You can duplicate your tables, just like you would inside a spreadsheet.
|
||||
Extensions are easier to use. You can enable Postgres extensions with the click of a button.
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source src="/videos/toggle-extensions.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/toggle-extensions.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Save your favorite queries
|
||||
@@ -54,7 +64,12 @@ Extensions are easier to use. You can enable Postgres extensions with the click
|
||||
The SQL editor now stores your query history in your browser. You can also save your favorite queries to run later!
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source src="/videos/favorites.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/favorites.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### GitHub Discussions
|
||||
|
||||
@@ -74,7 +74,12 @@ Thanks to some skilled haxors (well, weak passwords mainly) we had crypto miners
|
||||
The spreadsheet import is triggered when the user is creating a new table. However the code was previously loaded every time the page was visited - even when a new table was not being created. This made it a good candidate for lazy loading. Using [Next.js dynamic imports](https://nextjs.org/docs/advanced-features/dynamic-import) we are able to load this component (313 kb brotlied) dynamically, whenever the user clicks the "Add content" button.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/videos/lazy_loading.mov" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/lazy_loading.mov"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
We use the same technique to lazy load some Lottie animations which are relatively large.
|
||||
|
||||
@@ -41,7 +41,12 @@ We spent months working on Performance, Security, and Reliability. Read more on
|
||||
Add comments and descriptions to your Tables directly from our auto-generated docs. Descriptions are stored as PostgreSQL comments (https://postgresql.org/docs/current/sql-comment.html), and are exposed over your OpenAPI spec.
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source src="/videos/update-docs.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/update-docs.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Table View now has realtime changes
|
||||
@@ -49,7 +54,12 @@ Add comments and descriptions to your Tables directly from our auto-generated do
|
||||
Any updates that happen to your database are reflected in the Table View immediately.
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source src="/videos/realtime-updates.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/realtime-updates.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Table Pagination
|
||||
@@ -57,7 +67,12 @@ Any updates that happen to your database are reflected in the Table View immedia
|
||||
Our table view now has pagination - better for working with large data sets.
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source src="/videos/table-pagination.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/table-pagination.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Supabase raised a Seed Round
|
||||
|
||||
@@ -21,7 +21,7 @@ Clients can use the `Content-Range` HTTP header to request specific parts of a f
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source
|
||||
src="/videos/blog/launch-week-storage-2/nyan-cat.mp4"
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-storage-2/nyan-cat.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
@@ -50,7 +50,7 @@ You can now upload an entire directory to Supabase Storage and the directory str
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source
|
||||
src="/videos/blog/launch-week-storage-2/directory-uploads.mp4"
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-storage-2/directory-uploads.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
@@ -121,7 +121,7 @@ Here is a demo testing rendering the heaviest object known to mankind - the `nod
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source
|
||||
src="/videos/blog/launch-week-storage-2/large-buckets.mp4"
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-storage-2/large-buckets.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
|
||||
@@ -225,7 +225,7 @@ To enable GraphQL in your Supabase instance, enable `pg_graphql` from the dashbo
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source
|
||||
src="/videos/blog/launch-week-4/api-enable-graphql.mp4"
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/api-enable-graphql.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
|
||||
@@ -124,7 +124,12 @@ Some features are too good to limit to large customers, so today we're introduci
|
||||
### Spend caps
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source src="/videos/blog/launch-week-4/spend-caps.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/spend-caps.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
To simplify pricing, we've merged the “Pro” and “Pay as you go” tiers and introduced monthly spend caps to avoid nasty billing surprises.
|
||||
@@ -136,7 +141,12 @@ We're also retaining our soft limits while we manage the transition to granular
|
||||
### Database Add-ons
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source src="/videos/blog/launch-week-4/database-addons.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/database-addons.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
Today we're releasing self-serve Database Add-ons.
|
||||
@@ -149,7 +159,12 @@ Today, Database Add-ons are available for a small set of customers. We will prog
|
||||
### New Log Explorer
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source src="/videos/blog/launch-week-4/logexplorer.mp4" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/logexplorer.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
Today we're releasing a brand new [Log Explorer](/docs/guides/platform/logs) in the Supabase Dashboard.
|
||||
|
||||
@@ -81,7 +81,7 @@ We've [extended the Supabase CLI](https://supabase.com/docs/guides/local-develop
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source
|
||||
src="/videos/blog/launch-week-4/functions-hot-reloading.mp4"
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/functions-hot-reloading.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
@@ -91,7 +91,12 @@ We've [extended the Supabase CLI](https://supabase.com/docs/guides/local-develop
|
||||
Once you've tested your Function locally (or gone straight to production cos you live _on the edge_ ... get it?) it takes just seconds to deploy a new function and see the logs streaming in your Dashboard—ensuring a tight iteration loop with your production environment.
|
||||
|
||||
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
|
||||
<source src="/videos/blog/launch-week-4/logs-polish.mov" type="video/mp4" muted playsInline />
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/launch-week-4/logs-polish.mov"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
Our observability pipeline has been built [using Logflare](https://supabase.com/blog/supabase-acquires-logflare). Structured logs from your Edge Functions get sent to Logflare, which also processes them into metrics. Your Supabase Dashboard is able to reach out to Logflare endpoints, and with some of the [(not so) secret sauce](https://supabase.com/blog/supabase-studio) from our front-end team, you get responsive charts and logs for your Edge Functions.
|
||||
|
||||
@@ -1351,7 +1351,7 @@ We used bloc for our state management solution. One thing we could have done dif
|
||||
|
||||
We could also explore some cool feature improvement.
|
||||
At the top of the rooms page, we are loading the newest created users to start a conversation. This is fine, but it only allows users to start a conversation with new users.
|
||||
We can for example update this to a list of users that are online at the same time. We can implement this using the [presence feature](https://supabase.com/docs/guides/realtime#presence) of Supabase.
|
||||
We can for example update this to a list of users that are online at the same time. We can implement this using the [presence feature](https://supabase.com/docs/guides/realtime/presence) of Supabase.
|
||||
|
||||
## More Flutter Resources
|
||||
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
---
|
||||
title: Storing OpenAI embeddings in Postgres with pgvector
|
||||
description: An example of how to build an AI-powered search engine using OpenAI's embeddings and PostgreSQL.
|
||||
author: gregnr
|
||||
image: embeddings/og_pgvector.png
|
||||
thumb: embeddings/og_pgvector.png
|
||||
tags:
|
||||
- postgres
|
||||
- ai
|
||||
date: '2023-01-06'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
A new PostgreSQL extension is now available in Supabase: [`pgvector`](https://github.com/pgvector/pgvector), an open-source vector similarity search.
|
||||
|
||||
The exponential progress of AI functionality over the past year has inspired many new real world applications. One specific challenge has been the ability to store and query _embeddings_ at scale.
|
||||
In this post we'll explain what embeddings are, why we might want to use them, and how we can store and query them in PostgreSQL using `pgvector`.
|
||||
|
||||
## What are embeddings?
|
||||
|
||||
Embeddings capture the “relatedness” of text, images, video, or other types of information. This relatedness is most commonly used for:
|
||||
|
||||
- **Search:** how similar is a search term to a body of text?
|
||||
- **Recommendations:** how similar are two products?
|
||||
- **Classifications:** how do we categorize a body of text?
|
||||
- **Clustering:** how do we identify trends?
|
||||
|
||||
Let's explore an example of text embeddings. Say we have three phrases:
|
||||
|
||||
1. “The cat chases the mouse”
|
||||
2. “The kitten hunts rodents”
|
||||
3. “I like ham sandwiches”
|
||||
|
||||
Your job is to group phrases with similar meaning. If you are a human, this should be obvious. Phrases 1 and 2 are almost identical, while phrase 3 has a completely different meaning.
|
||||
|
||||
Although phrases 1 and 2 are similar, they share no common vocabulary (besides “the”). Yet their meanings are nearly identical. How can we teach a computer that these are the same?
|
||||
|
||||
## Human language
|
||||
|
||||
Humans use words and symbols to communicate language. But words in isolation are mostly meaningless - we need to draw from shared knowledge & experience in order to make sense of them. The phrase “You should Google it” only makes sense if you know that Google is a search engine and that people have been using it as a verb.
|
||||
|
||||
In the same way, we need to train a neural network model to understand human language. An effective model should be trained on millions of different examples to understand what each word, phrase, sentence, or paragraph could mean in different contexts.
|
||||
|
||||
So how does this relate to embeddings?
|
||||
|
||||
## How do embeddings work?
|
||||
|
||||
Embeddings compress discrete information (words & symbols) into distributed continuous-valued data (vectors). If we took our phrases from before and plot them on a chart, it might look something like this:
|
||||
|
||||

|
||||
|
||||
Phrases 1 and 2 would be plotted close to each other, since their meanings are similar. We would expect phrase 3 to live somewhere far away since it isn't related. If we had a fourth phrase, “Sally ate Swiss cheese”, this might exist somewhere between phrase 3 (cheese can go on sandwiches) and phrase 1 (mice like Swiss cheese).
|
||||
|
||||
In this example we only have 2 dimensions: the X and Y axis. In reality, we would need many more dimensions to effectively capture the complexities of human language.
|
||||
|
||||
## OpenAI embeddings
|
||||
|
||||
OpenAI offers an [API](https://platform.openai.com/docs/guides/embeddings) to generate embeddings for a string of text using its language model. You feed it any text information (blog articles, documentation, your company's knowledge base), and it will output a vector of floating point numbers that represents the “meaning” of that text.
|
||||
|
||||
Compared to our 2-dimensional example above, their latest embedding model `text-embedding-ada-002` will output 1536 dimensions.
|
||||
|
||||
Why is this useful? Once we have generated embeddings on multiple texts, it is trivial to calculate how similar they are using vector math operations like cosine distance. A perfect use case for this is search. Your process might look something like this:
|
||||
|
||||
1. Pre-process your knowledge base and generate embeddings for each page
|
||||
2. Store your embeddings to be referenced later (more on this)
|
||||
3. Build a search page that prompts your user for input
|
||||
4. Take user's input, generate a one-time embedding, then perform a similarity search against your pre-processed embeddings.
|
||||
5. Return the most similar pages to the user
|
||||
|
||||
## Embeddings in practice
|
||||
|
||||
At a small scale, you could store your embeddings in a CSV file, load them into Python, and use a library like `numPy` to calculated similarity between them using something like cosine distance or dot product. OpenAI has a cookbook [example](https://github.com/openai/openai-cookbook/blob/main/examples/Semantic_text_search_using_embeddings.ipynb) that does just that. Unfortunately this likely won't scale well:
|
||||
|
||||
- What if I need to store and search over a large number of documents and embeddings (more than can fit in memory)?
|
||||
- What if I want to create/update/delete embeddings dynamically?
|
||||
- What if I'm not using Python?
|
||||
|
||||
### Using PostgreSQL
|
||||
|
||||
Enter [`pgvector`](https://github.com/pgvector/pgvector), an extension for PostgreSQL that allows you to both store and query vector embeddings within your database. Let's try it out.
|
||||
|
||||
First we'll enable the **Vector** extension. In Supabase, this can be done from the web portal through `Database` → `Extensions`. You can also do this in SQL by running:
|
||||
|
||||
```sql
|
||||
create extension vector;
|
||||
```
|
||||
|
||||
Next let's create a table to store our documents and their embeddings:
|
||||
|
||||
```sql
|
||||
create table documents (
|
||||
id bigserial primary key,
|
||||
content text,
|
||||
embedding vector (1536)
|
||||
);
|
||||
```
|
||||
|
||||
`pgvector` introduces a new data type called `vector`. In the code above, we create a column named `embedding` with the `vector` data type. The size of the vector defines how many dimensions the vector holds. OpenAI's `text-embedding-ada-002` model outputs 1536 dimensions, so we will use that for our vector size.
|
||||
|
||||
We also create a `text` column named `content` to store the original document text that produced this embedding. Depending on your use case, you might just store a reference (URL or foreign key) to a document here instead.
|
||||
|
||||
Soon we're going to need to perform a similarity search over these embeddings. Let's create a function to do that:
|
||||
|
||||
```sql
|
||||
create or replace function match_documents (
|
||||
query_embedding vector(1536),
|
||||
similarity_threshold float,
|
||||
match_count int
|
||||
)
|
||||
returns table (
|
||||
id bigint,
|
||||
content text,
|
||||
similarity float
|
||||
)
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select
|
||||
id,
|
||||
content,
|
||||
(documents.embedding <=> query_embedding) as similarity
|
||||
from documents
|
||||
where (documents.embedding <=> query_embedding) > similarity_threshold
|
||||
order by documents.embedding <=> query_embedding
|
||||
limit match_count;
|
||||
end;
|
||||
$$;
|
||||
```
|
||||
|
||||
`pgvector` introduces 3 new operators that can be used to calculate similarity:
|
||||
|
||||
| Operator | Description |
|
||||
| -------- | ---------------------- |
|
||||
| `<->` | Euclidean distance |
|
||||
| `<#>` | negative inner product |
|
||||
| `<=>` | cosine distance |
|
||||
|
||||
OpenAI recommends cosine similarity on their embeddings, so we will use that here.
|
||||
|
||||
Now we can call `match_documents()`, pass in our embedding, similarity threshold, and match count, and we'll get a list of all documents that match. And since this is all managed by Postgres, our application code becomes very simple.
|
||||
|
||||
### Indexing
|
||||
|
||||
Once your table starts to grow with embeddings, you will likely want to add an index to speed up queries. Vector indexes are particularly important when you're ordering results because vectors are not grouped by similarity, so finding the closest by sequential scan is a resource-intensive operation.
|
||||
|
||||
Each distance operator requires a different type of index. We expect to order by cosine distance, so we need `vector_cosine_ops` index. A good starting number of lists is 4 \* sqrt(table_rows):
|
||||
|
||||
```sql
|
||||
create index on documents
|
||||
using ivfflat (embedding vector_cosine_ops)
|
||||
with (lists = 100);
|
||||
```
|
||||
|
||||
You can read more about indexing on `pgvector`'s GitHub page [here](https://github.com/pgvector/pgvector#indexing).
|
||||
|
||||
### Generating embeddings
|
||||
|
||||
Let's use JavaScript to generate embeddings and store them in Postgres:
|
||||
|
||||
```tsx
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Configuration, OpenAIApi } from 'openai'
|
||||
import { supabaseClient } from './lib/supabase'
|
||||
|
||||
async function generateEmbeddings() {
|
||||
const configuration = new Configuration({ apiKey: '<YOUR_OPENAI_KEY>' })
|
||||
const openAi = new OpenAIApi(configuration)
|
||||
|
||||
const documents = await getDocuments() // Your custom function to load docs
|
||||
|
||||
// Assuming each document is a string
|
||||
for (const document of documents) {
|
||||
// OpenAI recommends replacing newlines with spaces for best results
|
||||
const input = document.replace(/\n/g, ' ')
|
||||
|
||||
const embeddingResponse = await openai.createEmbedding({
|
||||
model: 'text-embedding-ada-002',
|
||||
input,
|
||||
})
|
||||
|
||||
const [{ embedding }] = embeddingResponse.data.data
|
||||
|
||||
// In production we should handle possible errors
|
||||
await supabaseClient.from('documents').insert({
|
||||
content: document,
|
||||
embedding,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Building a simple search function
|
||||
|
||||
Finally, let's create an [Edge Function](https://supabase.com/docs/guides/functions) to perform our similarity search:
|
||||
|
||||
```tsx
|
||||
import { serve } from 'https://deno.land/std@0.170.0/http/server.ts'
|
||||
import 'https://deno.land/x/xhr@0.2.1/mod.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.5.0'
|
||||
import { Configuration, OpenAIApi } from 'https://esm.sh/openai@3.1.0'
|
||||
import { supabaseClient } from './lib/supabase'
|
||||
|
||||
export const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
// Handle CORS
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
// Search query is passed in request payload
|
||||
const { query } = await req.json()
|
||||
|
||||
// OpenAI recommends replacing newlines with spaces for best results
|
||||
const input = query.replace(/\n/g, ' ')
|
||||
|
||||
const configuration = new Configuration({ apiKey: '<YOUR_OPENAI_KEY>' })
|
||||
const openai = new OpenAIApi(configuration)
|
||||
|
||||
// Generate a one-time embedding for the query itself
|
||||
const embeddingResponse = await openai.createEmbedding({
|
||||
model: 'text-embedding-ada-002',
|
||||
input,
|
||||
})
|
||||
|
||||
const [{ embedding }] = embeddingResponse.data.data
|
||||
|
||||
// In production we should handle possible errors
|
||||
const { data: documents } = await supabaseClient.rpc('match_documents', {
|
||||
query_embedding: embedding,
|
||||
match_threshold: 0.78, // Choose an appropriate threshold for your data
|
||||
match_count: 10, // Choose the number of matches
|
||||
})
|
||||
|
||||
return new Response(JSON.stringify(documents), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Building a smarter search function
|
||||
|
||||
ChatGPT doesn't just return existing documents. It's able to assimilate a variety of information into a single, cohesive answer. To do this, we need to provide GPT with some relevant documents, and a prompt that it can use to formulate this answer.
|
||||
|
||||
One of the biggest challenges of OpenAI's `text-davinci-003` [completion model](https://beta.openai.com/docs/guides/completion) is the 4000 token limit. You must fit both your prompt and the resulting completion within the 4000 tokens. This makes it challenging if you wanted to prompt GPT-3 to answer questions about your own custom knowledge base that would never fit in a single prompt.
|
||||
|
||||
Embeddings can help solve this by splitting your prompts into a two-phased process:
|
||||
|
||||
1. Query your embedding database for the most relevant documents related to the question
|
||||
2. Inject these documents as context for GPT-3 to reference in its answer
|
||||
|
||||
Here's another Edge Function that expands upon the simple example above:
|
||||
|
||||
```tsx
|
||||
import { serve } from 'https://deno.land/std@0.170.0/http/server.ts'
|
||||
import 'https://deno.land/x/xhr@0.2.1/mod.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.5.0'
|
||||
import GPT3Tokenizer from 'https://esm.sh/gpt3-tokenizer@1.1.5'
|
||||
import { Configuration, OpenAIApi } from 'https://esm.sh/openai@3.1.0'
|
||||
import { stripIndent } from 'https://esm.sh/common-tags@1.8.2'
|
||||
import { supabaseClient } from './lib/supabase'
|
||||
|
||||
export const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
// Handle CORS
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
// Search query is passed in request payload
|
||||
const { query } = await req.json()
|
||||
|
||||
// OpenAI recommends replacing newlines with spaces for best results
|
||||
const input = query.replace(/\n/g, ' ')
|
||||
|
||||
const configuration = new Configuration({ apiKey: '<YOUR_OPENAI_KEY>' })
|
||||
const openai = new OpenAIApi(configuration)
|
||||
|
||||
// Generate a one-time embedding for the query itself
|
||||
const embeddingResponse = await openai.createEmbedding({
|
||||
model: 'text-embedding-ada-002',
|
||||
input,
|
||||
})
|
||||
|
||||
const [{ embedding }] = embeddingResponse.data.data
|
||||
|
||||
// Fetching whole documents for this simple example.
|
||||
//
|
||||
// Ideally for context injection, documents are chunked into
|
||||
// smaller sections at earlier pre-processing/embedding step.
|
||||
const { data: documents } = await supabaseClient.rpc('match_documents', {
|
||||
query_embedding: embedding,
|
||||
match_threshold: 0.78, // Choose an appropriate threshold for your data
|
||||
match_count: 10, // Choose the number of matches
|
||||
})
|
||||
|
||||
const tokenizer = new GPT3Tokenizer({ type: 'gpt3' })
|
||||
let tokenCount = 0
|
||||
let contextText = ''
|
||||
|
||||
// Concat matched documents
|
||||
for (let i = 0; i < documents.length; i++) {
|
||||
const document = documents[i]
|
||||
const content = document.content
|
||||
const encoded = tokenizer.encode(content)
|
||||
tokenCount += encoded.text.length
|
||||
|
||||
// Limit context to max 1500 tokens (configurable)
|
||||
if (tokenCount > 1500) {
|
||||
break
|
||||
}
|
||||
|
||||
contextText += `${content.trim()}\n---\n`
|
||||
}
|
||||
|
||||
const prompt = stripIndent`${oneLine`
|
||||
You are a very enthusiastic Supabase representative who loves
|
||||
to help people! Given the following sections from the Supabase
|
||||
documentation, answer the question using only that information,
|
||||
outputted in markdown format. If you are unsure and the answer
|
||||
is not explicitly written in the documentation, say
|
||||
"Sorry, I don't know how to help with that."`}
|
||||
|
||||
Context sections:
|
||||
${contextText}
|
||||
|
||||
Question: """
|
||||
${query}
|
||||
"""
|
||||
|
||||
Answer as markdown (including related code snippets if available):
|
||||
`
|
||||
|
||||
// In production we should handle possible errors
|
||||
const completionResponse = await openai.createCompletion({
|
||||
model: 'text-davinci-003',
|
||||
prompt,
|
||||
max_tokens: 512, // Choose the max allowed tokens in completion
|
||||
temperature: 0, // Set to 0 for deterministic results
|
||||
})
|
||||
|
||||
const {
|
||||
id,
|
||||
choices: [{ text }],
|
||||
} = completionResponse.data
|
||||
|
||||
return new Response(JSON.stringify({ id, text }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Streaming results
|
||||
|
||||
OpenAI API responses take longer to depending on the length of the “answer”. ChatGPT has a nice UX for this by streaming the response to the user immediately. You can see a similar effect for the Supabase docs:
|
||||
|
||||
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls="true">
|
||||
<source src="/images/blog/embeddings/embeddings.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
The OpenAI API supports [completion streaming](https://platform.openai.com/docs/api-reference/completions/create#completions/create-stream) with Server Side Events. Supabase Edge Functions are run Deno, which also supports [Server Side Events](https://deno.com/blog/deploy-streams#server-sent-events). Check out [this commit](https://github.com/supabase/supabase/pull/12056/commits/bd83e9ba2f7263440888228e3b29007604d94841) to see how we modified the Function above to build a streaming interface.
|
||||
|
||||
## Wrap up
|
||||
|
||||
Storing embeddings in Postgres opens a world of possibilities. You can combine your search function with telemetry functions, add an user-provided feedback (thumbs up/down), and make your search feel more integrated with your products.
|
||||
|
||||
Tomorrow we'll be releasing our own version of this interface in the Supabase docs, to provide Supabase-specific answers.
|
||||
|
||||
The pgvector extension is available on all new Supabase projects today. If you want to try it out, launch a new Postgres database today: [database.new](https://database.new)
|
||||
@@ -11,7 +11,7 @@
|
||||
"label": "SQL Editor",
|
||||
"title": "In-built SQL editor for when you need greater control",
|
||||
"text": "Write, save, and execute SQL queries directly on our dashboard, with templates to save you time. Run common queries and even build applications using our growing list of templates.",
|
||||
"video_url": "videos/tabSqlEditor.mp4",
|
||||
"video_url": "https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/tabSqlEditor.mp4",
|
||||
"youtube_id": "Hch1mZPZ53A",
|
||||
"cta": "Explore SQL Editor",
|
||||
"url": "/database"
|
||||
@@ -20,7 +20,7 @@
|
||||
"label": "Auth rules",
|
||||
"title": "User management as straight-forward as it can be",
|
||||
"text": "Easily manage your users with Supabase Auth, with email logins, magic links, and third-party logins. Create complex access policies with SQL rules to fit your unique business needs.",
|
||||
"video_url": "videos/tabAuthRules.mp4",
|
||||
"video_url": "https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/tabAuthRules.mp4",
|
||||
"youtube_id": "vP319FCIZ6Y",
|
||||
"cta": "Explore Auth",
|
||||
"url": "/auth"
|
||||
|
||||
@@ -264,5 +264,12 @@
|
||||
"position": "Engineering",
|
||||
"author_url": "https://github.com/hf",
|
||||
"author_image_url": "https://github.com/hf.png"
|
||||
},
|
||||
{
|
||||
"author_id": "gregnr",
|
||||
"author": "Greg Richardson",
|
||||
"position": "Open Source Contributor",
|
||||
"author_url": "https://github.com/gregnr",
|
||||
"author_image_url": "https://github.com/gregnr.png"
|
||||
}
|
||||
]
|
||||
+16
-11
@@ -1220,13 +1220,13 @@ module.exports = [
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/realtime/broadcast',
|
||||
destination: '/docs/guides/realtime#broadcast',
|
||||
source: '/docs/guides/realtime#broadcast',
|
||||
destination: '/docs/guides/realtime/broadcast',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/realtime/presence',
|
||||
destination: '/docs/guides/realtime#presence',
|
||||
source: '/docs/guides/realtime#presence',
|
||||
destination: '/docs/guides/realtime/presence',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
@@ -1780,37 +1780,37 @@ module.exports = [
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/javascript',
|
||||
destination: '/docs/reference/javascript/introduction',
|
||||
destination: '/docs/reference/javascript/start',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/dart',
|
||||
destination: '/docs/reference/dart/introduction',
|
||||
destination: '/docs/reference/dart/start',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/cli',
|
||||
destination: '/docs/reference/cli/introduction',
|
||||
destination: '/docs/reference/cli/start',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/api',
|
||||
destination: '/docs/reference/api/introduction',
|
||||
destination: '/docs/reference/api/start',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/auth',
|
||||
destination: '/docs/reference/auth/introduction',
|
||||
destination: '/docs/reference/self-hosting-auth/start',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/storage',
|
||||
destination: '/docs/reference/storage/introduction',
|
||||
destination: '/docs/reference/self-hosting-storage/start',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/realtime',
|
||||
destination: '/docs/reference/realtime/introduction',
|
||||
destination: '/docs/reference/self-hosting-realtime/start',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
@@ -1837,4 +1837,9 @@ module.exports = [
|
||||
source: '/docs/reference/dart/upgrade-guide',
|
||||
destination: '/docs/reference/dart/v0/upgrade-guide',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/examples',
|
||||
destination: '/docs/guides/resources/examples',
|
||||
},
|
||||
]
|
||||
+74
-75
@@ -28,7 +28,7 @@ const NavFlyOutMenu = (props: any) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-dark-300 dark:border-dark-400 col-span-12 grid grid-cols-12 items-center border-b border-dashed sm:border-r">
|
||||
<div className="grid items-center grid-cols-12 col-span-12 border-b border-dashed border-dark-300 dark:border-dark-400 sm:border-r">
|
||||
{segments.flat().map((segment: any) => (
|
||||
<div
|
||||
key={segment.key}
|
||||
@@ -38,7 +38,7 @@ const NavFlyOutMenu = (props: any) => {
|
||||
bg-white px-4 py-4 sm:border-l lg:col-span-4 lg:px-6 lg:py-10`}
|
||||
>
|
||||
<p className="flex items-center text-black dark:text-white">
|
||||
<span className="text-dark-300 dark:text-dark-400 mr-2 font-mono text-xs">
|
||||
<span className="mr-2 font-mono text-xs text-dark-300 dark:text-dark-400">
|
||||
0{segment.no}
|
||||
</span>
|
||||
<span>{segment.name}</span>
|
||||
@@ -53,12 +53,12 @@ const NavFlyOutMenu = (props: any) => {
|
||||
const VideoShot = (props: any) => {
|
||||
const { src } = props
|
||||
return (
|
||||
<div className="rounded-xl bg-gradient-to-r from-green-400 via-violet-500 to-blue-500 p-2">
|
||||
<div className="p-2 rounded-xl bg-gradient-to-r from-green-400 via-violet-500 to-blue-500">
|
||||
<div className="rounded-md shadow-lg" style={{ height: 'fit-content' }}>
|
||||
<div className="bg-dark-400 flex h-5 w-full items-center justify-start rounded-t-md px-2">
|
||||
<div className="bg-dark-500 mr-2 h-2 w-2 rounded-full" />
|
||||
<div className="bg-dark-500 mr-2 h-2 w-2 rounded-full" />
|
||||
<div className="bg-dark-500 mr-2 h-2 w-2 rounded-full" />
|
||||
<div className="flex items-center justify-start w-full h-5 px-2 bg-dark-400 rounded-t-md">
|
||||
<div className="w-2 h-2 mr-2 rounded-full bg-dark-500" />
|
||||
<div className="w-2 h-2 mr-2 rounded-full bg-dark-500" />
|
||||
<div className="w-2 h-2 mr-2 rounded-full bg-dark-500" />
|
||||
</div>
|
||||
<video className="rounded-b-md" src={src} autoPlay loop muted playsInline>
|
||||
Your browser does not support the video tag
|
||||
@@ -94,7 +94,7 @@ const SectionHeader = (props: any) => {
|
||||
const { sectionNumber, header } = props
|
||||
return (
|
||||
<div className="col-span-12">
|
||||
<span className="text-scale-1200 mb-3 block font-mono text-3xl">0{sectionNumber}</span>
|
||||
<span className="block mb-3 font-mono text-3xl text-scale-1200">0{sectionNumber}</span>
|
||||
<h3 className="text-6xl text-black dark:text-white">{header}</h3>
|
||||
</div>
|
||||
)
|
||||
@@ -112,10 +112,10 @@ const Hero = (props: Props) => {
|
||||
backgroundSize: '65%',
|
||||
backgroundPosition: '120% 50%',
|
||||
}}
|
||||
className="dark:bg-scale-300 bg-gray-50 bg-no-repeat py-16 lg:py-36"
|
||||
className="py-16 bg-no-repeat dark:bg-scale-300 bg-gray-50 lg:py-36"
|
||||
>
|
||||
<div className="text-dark-300 container mx-auto grid h-full grid-cols-12 items-center gap-4 px-8 py-20 lg:px-28">
|
||||
<div className="text-scale-900 col-span-12 dark:text-white md:col-span-9 lg:col-span-8 xl:col-span-6">
|
||||
<div className="container grid items-center h-full grid-cols-12 gap-4 px-8 py-20 mx-auto text-dark-300 lg:px-28">
|
||||
<div className="col-span-12 text-scale-900 dark:text-white md:col-span-9 lg:col-span-8 xl:col-span-6">
|
||||
<p className="mb-10 text-4xl">Supabase is an open source Firebase alternative.</p>
|
||||
<p className="text-2xl">
|
||||
Today, we're moving to <span className="text-brand-900">Beta</span>
|
||||
@@ -131,10 +131,10 @@ const Hero = (props: Props) => {
|
||||
|
||||
const Introduction = () => {
|
||||
return (
|
||||
<div className="text-dark-300 container mx-auto grid grid-cols-12 gap-4 px-8 py-20 lg:px-28">
|
||||
<div className="container grid grid-cols-12 gap-4 px-8 py-20 mx-auto text-dark-300 lg:px-28">
|
||||
<div className="col-span-12 sm:col-span-9 xl:col-span-8">
|
||||
<p>
|
||||
<span className="text-scale-1200 block text-2xl">
|
||||
<span className="block text-2xl text-scale-1200">
|
||||
After the launch of our{' '}
|
||||
<a
|
||||
href="https://news.ycombinator.com/item?id=23319901"
|
||||
@@ -147,7 +147,7 @@ const Introduction = () => {
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-scale-1200 col-span-12 mb-20 text-base sm:col-span-9 xl:col-span-6">
|
||||
<div className="col-span-12 mb-20 text-base text-scale-1200 sm:col-span-9 xl:col-span-6">
|
||||
<p>
|
||||
we've been fortunate to work with thousands of early adopters on improving both our Open
|
||||
Source, and Hosted offerings. Companies like{' '}
|
||||
@@ -179,35 +179,35 @@ const Introduction = () => {
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 mb-10 text-base">
|
||||
<p className="border-dark-200 text-scale-1100 dark:border-dark-400 w-60 border-b-2 pb-2">
|
||||
<p className="pb-2 border-b-2 border-dark-200 text-scale-1100 dark:border-dark-400 w-60">
|
||||
Alpha Program in Numbers
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="alphaNumbers"
|
||||
className="col-span-12 mb-20 grid grid-cols-12 gap-y-12 gap-x-3 sm:gap-x-0 lg:gap-y-20"
|
||||
className="grid grid-cols-12 col-span-12 mb-20 gap-y-12 gap-x-3 sm:gap-x-0 lg:gap-y-20"
|
||||
>
|
||||
{AlphaNumbers.map((stat: any, idx: number) => (
|
||||
<div
|
||||
key={`stat_${idx}`}
|
||||
className="col-span-6 grid grid-cols-8 items-center gap-x-1 sm:col-span-4 sm:grid-cols-12 md:gap-x-0 xl:gap-x-6"
|
||||
className="grid items-center grid-cols-8 col-span-6 gap-x-1 sm:col-span-4 sm:grid-cols-12 md:gap-x-0 xl:gap-x-6"
|
||||
>
|
||||
<div className="col-span-4 sm:col-span-4 md:col-span-3 xl:col-span-2">
|
||||
<div className="bg-scale-1200 flex h-12 w-12 items-center justify-center rounded-md dark:bg-white">
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-md bg-scale-1200 dark:bg-white">
|
||||
{stat.icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sm-col-span-8 text-dark-400 col-span-7 md:col-span-9 xl:col-span-10">
|
||||
<p className="text-scale-1200 text-5xl lg:text-6xl">
|
||||
<div className="col-span-7 sm-col-span-8 text-dark-400 md:col-span-9 xl:col-span-10">
|
||||
<p className="text-5xl text-scale-1200 lg:text-6xl">
|
||||
<CountUp triggerAnimOnScroll={true} referenceElId="alphaNumbers">
|
||||
{stat.value}
|
||||
</CountUp>
|
||||
{stat.unit && <span className="text-scale-1200 ml-1 text-2xl">{stat.unit}</span>}
|
||||
{stat.unit && <span className="ml-1 text-2xl text-scale-1200">{stat.unit}</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-start-0 col-span-12 sm:col-span-9 sm:col-start-5 md:col-span-10 md:col-start-4 xl:col-start-3">
|
||||
<p className="text-scale-1000 text-xs lg:text-base">{stat.name}</p>
|
||||
<div className="col-span-12 col-start-0 sm:col-span-9 sm:col-start-5 md:col-span-10 md:col-start-4 xl:col-start-3">
|
||||
<p className="text-xs text-scale-1000 lg:text-base">{stat.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -220,29 +220,29 @@ const TableOfContents = (props: any) => {
|
||||
const { scrollTo } = props
|
||||
return (
|
||||
<div className="dark:bg-scale-300 text-dark-400 dark:text-dark-200 bg-gray-50">
|
||||
<div className="container mx-auto grid grid-cols-12 gap-4 px-8 py-20 lg:px-28">
|
||||
<div className="container grid grid-cols-12 gap-4 px-8 py-20 mx-auto lg:px-28">
|
||||
<div className="col-span-12 mb-10 text-base">
|
||||
<p className="text-2xl text-black dark:text-white">
|
||||
Supabase <span className="text-brand-900 dark:text-brand-900">Beta</span> is starting
|
||||
now.
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-12 grid grid-cols-12 gap-y-10">
|
||||
<div className="grid grid-cols-12 col-span-12 gap-y-10">
|
||||
{IntroductionSegments.map((segment: any, segmentIdx: number) => (
|
||||
<div key={`introSegment_${segmentIdx}`} className="col-span-12 grid grid-cols-12">
|
||||
<div className="text-scale-1200 col-span-12 mb-10 sm:col-span-5 sm:mb-0 xl:col-span-4">
|
||||
<div key={`introSegment_${segmentIdx}`} className="grid grid-cols-12 col-span-12">
|
||||
<div className="col-span-12 mb-10 text-scale-1200 sm:col-span-5 sm:mb-0 xl:col-span-4">
|
||||
<p>{segment.description}</p>
|
||||
</div>
|
||||
<div className="hidden sm:col-span-1 sm:block xl:col-span-2" />
|
||||
<div className="col-span-12 flex flex-col sm:col-span-6">
|
||||
<div className="flex flex-col col-span-12 sm:col-span-6">
|
||||
{segment.chapters.map((chapter: any, idx: number) => (
|
||||
<div
|
||||
key={`section_select_${idx}`}
|
||||
className="mb-5 flex cursor-pointer items-center"
|
||||
className="flex items-center mb-5 cursor-pointer"
|
||||
onClick={() => scrollTo(chapter.key)}
|
||||
>
|
||||
<p className="text-scale-1200 font-mono text-xs">{`0${chapter.no}`}</p>
|
||||
<p className="hover:text-gray-1000 dark:hover:text-gray-1000 ml-4 border-b border-gray-400 text-base text-black transition dark:text-white">
|
||||
<p className="font-mono text-xs text-scale-1200">{`0${chapter.no}`}</p>
|
||||
<p className="ml-4 text-base text-black transition border-b border-gray-400 hover:text-gray-1000 dark:hover:text-gray-1000 dark:text-white">
|
||||
{chapter.name}
|
||||
</p>
|
||||
</div>
|
||||
@@ -302,13 +302,13 @@ const Performance = () => {
|
||||
{PerformanceComparisonData.map((metric: any) => {
|
||||
const multiplier = (metric.stats[0].value / metric.stats[1].value).toFixed(1)
|
||||
return (
|
||||
<div key={`${metric.key}`} className="text-dark-400 dark:text-dark-300 mb-10">
|
||||
<p className="mb-4 pb-2 sm:w-36">{metric.title}</p>
|
||||
<div key={`${metric.key}`} className="mb-10 text-dark-400 dark:text-dark-300">
|
||||
<p className="pb-2 mb-4 sm:w-36">{metric.title}</p>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center">
|
||||
<div className="w-full sm:w-5/6">
|
||||
{metric.stats.map((stat: any, idx: number) => (
|
||||
<div key={`metric_${metric.key}_${idx}`} className="flex items-center">
|
||||
<p className="mr-4 w-20 border-r py-2 pr-4 text-left sm:text-right lg:w-24">
|
||||
<p className="w-20 py-2 pr-4 mr-4 text-left border-r sm:text-right lg:w-24">
|
||||
{stat.name}
|
||||
</p>
|
||||
<Bar
|
||||
@@ -324,7 +324,7 @@ const Performance = () => {
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col text-left sm:w-1/6 sm:text-right">
|
||||
<p className="text-dark-700 dark:text-dark-100 text-6xl">{multiplier}x</p>
|
||||
<p className="text-6xl text-dark-700 dark:text-dark-100">{multiplier}x</p>
|
||||
<p className="-mt-2 text-sm">more {metric.key}s per second</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -336,11 +336,11 @@ const Performance = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="performance" className="dark:bg-scale-200 bg-white">
|
||||
<div className="text-dark-400 dark:text-dark-300 container mx-auto grid grid-cols-12 gap-y-10 px-8 py-20 lg:px-28">
|
||||
<div id="performance" className="bg-white dark:bg-scale-200">
|
||||
<div className="container grid grid-cols-12 px-8 py-20 mx-auto text-dark-400 dark:text-dark-300 gap-y-10 lg:px-28">
|
||||
<SectionHeader sectionNumber={1} header="Performance" />
|
||||
|
||||
<div className="text-scale-1200 col-span-12 mb-10 grid grid-cols-12 items-center gap-x-2 lg:gap-x-8">
|
||||
<div className="grid items-center grid-cols-12 col-span-12 mb-10 text-scale-1200 gap-x-2 lg:gap-x-8">
|
||||
<div className="col-span-12 text-base sm:col-span-9 xl:col-span-7">
|
||||
<p className="mb-10">
|
||||
We started Supabase to give developers a web-ready database that is delightful to use,
|
||||
@@ -420,10 +420,10 @@ const Performance = () => {
|
||||
const Security = () => {
|
||||
return (
|
||||
<div id="security" className="dark:bg-scale-300 bg-gray-50">
|
||||
<div className="text-dark-400 dark:text-dark-200 container mx-auto grid grid-cols-12 gap-y-10 px-8 py-20 lg:px-28">
|
||||
<div className="container grid grid-cols-12 px-8 py-20 mx-auto text-dark-400 dark:text-dark-200 gap-y-10 lg:px-28">
|
||||
<SectionHeader sectionNumber={2} header="Security" />
|
||||
|
||||
<div className="text-scale-1200 col-span-12 grid grid-cols-12 gap-x-2 lg:gap-x-8">
|
||||
<div className="grid grid-cols-12 col-span-12 text-scale-1200 gap-x-2 lg:gap-x-8">
|
||||
<div className="col-span-12 text-base sm:col-span-9 xl:col-span-7">
|
||||
<p className="mb-10">
|
||||
As an infrastructure provider, security has been a priority from day one. While we had
|
||||
@@ -434,7 +434,7 @@ const Security = () => {
|
||||
Approaching the launch of our Beta period, we worked with security advisors and
|
||||
specialists globally to enforce new measures and processes:
|
||||
</p>
|
||||
<ul className="ml-6 list-outside list-disc">
|
||||
<ul className="ml-6 list-disc list-outside">
|
||||
<li className="mb-5">
|
||||
Employed DigitalXRAID to run a full Pen Test on both our internal and customer
|
||||
infrastructure. We immediately patched one medium priority issue and are currently
|
||||
@@ -482,19 +482,18 @@ const Security = () => {
|
||||
}
|
||||
|
||||
const Reliability = () => {
|
||||
const { basePath } = useRouter()
|
||||
return (
|
||||
<div id="reliability" className="dark:bg-scale-200 bg-white">
|
||||
<div className="text-dark-400 dark:text-dark-300 container mx-auto grid grid-cols-12 gap-y-10 px-8 py-12 lg:px-28 ">
|
||||
<div id="reliability" className="bg-white dark:bg-scale-200">
|
||||
<div className="container grid grid-cols-12 px-8 py-12 mx-auto text-dark-400 dark:text-dark-300 gap-y-10 lg:px-28 ">
|
||||
<SectionHeader sectionNumber={3} header="Reliability" />
|
||||
|
||||
<div className="text-scale-1200 col-span-12 mb-10 grid grid-cols-12 gap-x-2 lg:gap-x-8">
|
||||
<div className="grid grid-cols-12 col-span-12 mb-10 text-scale-1200 gap-x-2 lg:gap-x-8">
|
||||
<div className="col-span-12 text-base sm:col-span-9 xl:col-span-7">
|
||||
<p className="mb-5">
|
||||
During Alpha we experienced 2 availability incidents, neither affecting customer
|
||||
access to their data. These were:
|
||||
</p>
|
||||
<ul className="mb-10 ml-6 list-outside list-disc">
|
||||
<ul className="mb-10 ml-6 list-disc list-outside">
|
||||
<li className="mb-5">
|
||||
A third-party CDN API outage. As a result, subdomains were not created for new
|
||||
projects.
|
||||
@@ -521,7 +520,7 @@ const Reliability = () => {
|
||||
to keep track of uptime across all of our services and critical infrastructure.
|
||||
</p>
|
||||
<div className="mb-10">
|
||||
<VideoShot src={`${basePath}/videos/statusPage.mp4`} />
|
||||
<VideoShot src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/statusPage.mp4" />
|
||||
</div>
|
||||
<p>
|
||||
For our Alpha & Beta Users we take free, encrypted daily database backups up to 20GB.
|
||||
@@ -538,10 +537,10 @@ const NewFeaturesAndIntegrations = () => {
|
||||
const { basePath } = useRouter()
|
||||
return (
|
||||
<div id="newFeaturesAndIntegrations" className="dark:bg-scale-300 bg-gray-50">
|
||||
<div className="text-dark-400 dark:text-dark-200 container mx-auto grid grid-cols-12 gap-y-10 px-8 py-20 lg:px-28">
|
||||
<div className="container grid grid-cols-12 px-8 py-20 mx-auto text-dark-400 dark:text-dark-200 gap-y-10 lg:px-28">
|
||||
<SectionHeader sectionNumber={4} header="New Features & Integrations" />
|
||||
|
||||
<div className="text-scale-1200 col-span-12 mb-10 grid grid-cols-12 gap-x-2 lg:gap-x-8">
|
||||
<div className="grid grid-cols-12 col-span-12 mb-10 text-scale-1200 gap-x-2 lg:gap-x-8">
|
||||
<div className="col-span-12 text-base sm:col-span-9 xl:col-span-7">
|
||||
<p className="mb-5">
|
||||
If you're new to Supabase, here's a few of the things you get when you choose us as
|
||||
@@ -549,7 +548,7 @@ const NewFeaturesAndIntegrations = () => {
|
||||
</p>
|
||||
<ul className="">
|
||||
<li className="mb-10">
|
||||
<p className="border-dark-200 dark:border-dark-400 mb-2 w-20 border-b-2 pb-2">
|
||||
<p className="w-20 pb-2 mb-2 border-b-2 border-dark-200 dark:border-dark-400">
|
||||
Auth
|
||||
</p>
|
||||
<p className="mb-5">
|
||||
@@ -577,10 +576,10 @@ const NewFeaturesAndIntegrations = () => {
|
||||
transactional emails for you. We support passwordless links, and we offer several
|
||||
OAuth providers including Google, GitHub, with more on the way.
|
||||
</p>
|
||||
<VideoShot src={`${basePath}/videos/tabAuthRules.mp4`} />
|
||||
<VideoShot src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/tabAuthRules.mp4" />
|
||||
</li>
|
||||
<li className="mb-10">
|
||||
<p className="border-dark-200 dark:border-dark-400 mb-2 w-20 border-b-2 pb-2">
|
||||
<p className="w-20 pb-2 mb-2 border-b-2 border-dark-200 dark:border-dark-400">
|
||||
Realtime
|
||||
</p>
|
||||
<p>
|
||||
@@ -598,7 +597,7 @@ const NewFeaturesAndIntegrations = () => {
|
||||
</p>
|
||||
</li>
|
||||
<li className="mb-10">
|
||||
<p className="border-dark-200 dark:border-dark-400 mb-2 w-24 border-b-2 pb-2">
|
||||
<p className="w-24 pb-2 mb-2 border-b-2 border-dark-200 dark:border-dark-400">
|
||||
CRUD API
|
||||
</p>
|
||||
<p>
|
||||
@@ -623,7 +622,7 @@ const NewFeaturesAndIntegrations = () => {
|
||||
</p>
|
||||
</li>
|
||||
<li className="mb-10">
|
||||
<p className="border-dark-200 dark:border-dark-400 mb-2 w-44 border-b-2 pb-2">
|
||||
<p className="pb-2 mb-2 border-b-2 border-dark-200 dark:border-dark-400 w-44">
|
||||
Quickstart Templates
|
||||
</p>
|
||||
<p>
|
||||
@@ -633,24 +632,24 @@ const NewFeaturesAndIntegrations = () => {
|
||||
</p>
|
||||
</li>
|
||||
<li className="mb-10">
|
||||
<p className="border-dark-200 dark:border-dark-400 mb-2 w-24 border-b-2 pb-2">
|
||||
<p className="w-24 pb-2 mb-2 border-b-2 border-dark-200 dark:border-dark-400">
|
||||
Table View
|
||||
</p>
|
||||
<p className="mb-5">
|
||||
View and edit your data like a spreadsheet from within the Supabase dashboard.
|
||||
Build your schema, create complex relationships, and import and export to csv.
|
||||
</p>
|
||||
<VideoShot src={`${basePath}/videos/tabTableEditor.mp4`} />
|
||||
<VideoShot src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/tabTableEditor.mp4" />
|
||||
</li>
|
||||
<li>
|
||||
<p className="border-dark-200 dark:border-dark-400 mb-2 w-24 border-b-2 pb-2">
|
||||
<p className="w-24 pb-2 mb-2 border-b-2 border-dark-200 dark:border-dark-400">
|
||||
SQL Editor
|
||||
</p>
|
||||
<p className="mb-5">
|
||||
No need to install third party SQL tools, you can run queries directly from the
|
||||
Supabase Dashboard.
|
||||
</p>
|
||||
<VideoShot src={`${basePath}/videos/tabSqlEditor.mp4`} />
|
||||
<VideoShot src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/tabSqlEditor.mp4" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -661,11 +660,11 @@ const NewFeaturesAndIntegrations = () => {
|
||||
}
|
||||
|
||||
const BetaPricing = () => (
|
||||
<div id="betaPricing" className="dark:bg-scale-200 bg-white">
|
||||
<div className="text-dark-400 dark:text-dark-300 container mx-auto grid grid-cols-12 gap-y-10 px-8 py-20 lg:px-28">
|
||||
<div id="betaPricing" className="bg-white dark:bg-scale-200">
|
||||
<div className="container grid grid-cols-12 px-8 py-20 mx-auto text-dark-400 dark:text-dark-300 gap-y-10 lg:px-28">
|
||||
<SectionHeader sectionNumber={5} header="Beta Pricing" />
|
||||
|
||||
<div className="text-scale-1200 col-span-12 mb-10 grid grid-cols-12 gap-x-2 lg:gap-x-8">
|
||||
<div className="grid grid-cols-12 col-span-12 mb-10 text-scale-1200 gap-x-2 lg:gap-x-8">
|
||||
<div className="col-span-12 text-base sm:col-span-9 xl:col-span-7">
|
||||
<p className="mb-10">For up to date pricing info see: https://supabase.com/pricing</p>
|
||||
<p className="mb-10">
|
||||
@@ -673,7 +672,7 @@ const BetaPricing = () => (
|
||||
course our Alpha Users, to provide a predictable and sustainable pricing model.
|
||||
</p>
|
||||
<p className="mb-5">Our key aims going into this exercise were:</p>
|
||||
<ul className="mb-10 ml-6 list-outside list-disc">
|
||||
<ul className="mb-10 ml-6 list-disc list-outside">
|
||||
<li className="mb-5">
|
||||
To continue offering free Supabase instances for Students, Hobbyists, and Early
|
||||
Adopters
|
||||
@@ -688,7 +687,7 @@ const BetaPricing = () => (
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-10">We are committing to the following initiatives:</p>
|
||||
<ul className="mb-10 ml-6 list-outside list-decimal">
|
||||
<ul className="mb-10 ml-6 list-decimal list-outside">
|
||||
<li className="mb-5">
|
||||
All Alpha Users will receive credits equivalent of 2 years of base tier usage. These
|
||||
will automatically be credited to your account if you signed up prior to December
|
||||
@@ -730,10 +729,10 @@ const BetaPricing = () => (
|
||||
|
||||
const OpenSource = () => (
|
||||
<div id="openSource" className="dark:bg-scale-300 bg-gray-50">
|
||||
<div className="text-dark-400 dark:text-dark-200 container mx-auto grid grid-cols-12 gap-y-10 px-8 py-20 lg:px-28">
|
||||
<div className="container grid grid-cols-12 px-8 py-20 mx-auto text-dark-400 dark:text-dark-200 gap-y-10 lg:px-28">
|
||||
<SectionHeader sectionNumber={6} header="Open Source" />
|
||||
|
||||
<div className="text-scale-1200 col-span-12 mb-10 grid grid-cols-12 gap-x-2 lg:gap-x-8">
|
||||
<div className="grid grid-cols-12 col-span-12 mb-10 text-scale-1200 gap-x-2 lg:gap-x-8">
|
||||
<div className="col-span-12 text-base sm:col-span-9 xl:col-span-7">
|
||||
<p className="mb-10">
|
||||
Great software is multi generational and stretches beyond any single company.
|
||||
@@ -796,10 +795,10 @@ const OpenSource = () => (
|
||||
)
|
||||
|
||||
const FundingPartners = () => (
|
||||
<div id="fundingPartners" className="dark:bg-scale-200 bg-white">
|
||||
<div className="text-dark-400 dark:text-dark-300 container mx-auto grid grid-cols-12 gap-y-10 px-8 py-20 lg:px-28">
|
||||
<div id="fundingPartners" className="bg-white dark:bg-scale-200">
|
||||
<div className="container grid grid-cols-12 px-8 py-20 mx-auto text-dark-400 dark:text-dark-300 gap-y-10 lg:px-28">
|
||||
<SectionHeader sectionNumber={7} header="Funding Partners" />
|
||||
<div className="text-scale-1200 col-span-12 mb-10 grid grid-cols-12 gap-x-2 lg:gap-x-8">
|
||||
<div className="grid grid-cols-12 col-span-12 mb-10 text-scale-1200 gap-x-2 lg:gap-x-8">
|
||||
<div className="col-span-12 text-base sm:col-span-9 xl:col-span-7">
|
||||
<p className="mb-10">
|
||||
Building a platform that can offer all the amazing features of Firebase will take
|
||||
@@ -834,10 +833,10 @@ const FundingPartners = () => (
|
||||
|
||||
const ScalingOurTeam = () => (
|
||||
<div id="scalingOurTeam" className="dark:bg-scale-300 bg-gray-50">
|
||||
<div className="text-dark-400 dark:text-dark-200 container mx-auto grid grid-cols-12 gap-y-10 px-8 py-20 lg:px-28">
|
||||
<div className="container grid grid-cols-12 px-8 py-20 mx-auto text-dark-400 dark:text-dark-200 gap-y-10 lg:px-28">
|
||||
<SectionHeader sectionNumber={8} header="Scaling Our Team" />
|
||||
|
||||
<div className="text-scale-1200 col-span-12 mb-10 grid grid-cols-12 gap-x-2 lg:gap-x-8">
|
||||
<div className="grid grid-cols-12 col-span-12 mb-10 text-scale-1200 gap-x-2 lg:gap-x-8">
|
||||
<div className="col-span-12 text-base sm:col-span-9 xl:col-span-7">
|
||||
<p className="mb-10">
|
||||
We are extremely proud of our team. We're a mix of 11 engineers, from 8 different
|
||||
@@ -872,12 +871,12 @@ const ScalingOurTeam = () => (
|
||||
const WhatsNext = () => (
|
||||
<div
|
||||
id="whatsNext"
|
||||
className="border-b-px dark:bg-scale-200 dark:border-scale-600 border-gray-50 bg-white"
|
||||
className="bg-white border-b-px dark:bg-scale-200 dark:border-scale-600 border-gray-50"
|
||||
>
|
||||
<div className="text-dark-400 dark:text-dark-300 container mx-auto grid grid-cols-12 gap-y-10 px-8 py-20 lg:px-28">
|
||||
<div className="container grid grid-cols-12 px-8 py-20 mx-auto text-dark-400 dark:text-dark-300 gap-y-10 lg:px-28">
|
||||
<SectionHeader sectionNumber={9} header="What's Next" />
|
||||
|
||||
<div className="text-scale-1200 col-span-12 mb-10 grid grid-cols-12 gap-x-2 lg:gap-x-8">
|
||||
<div className="grid grid-cols-12 col-span-12 mb-10 text-scale-1200 gap-x-2 lg:gap-x-8">
|
||||
<div className="col-span-12 text-base sm:col-span-9 xl:col-span-7">
|
||||
<p className="mb-5">
|
||||
If you watch our repos you'll see that development never stops, we ship every day (and
|
||||
@@ -885,7 +884,7 @@ const WhatsNext = () => (
|
||||
performance, security, and reliability improvements, here are some new features we're
|
||||
working on for 2021:
|
||||
</p>
|
||||
<ul className="mb-10 ml-6 list-outside list-disc">
|
||||
<ul className="mb-10 ml-6 list-disc list-outside">
|
||||
<li className="mb-5">Storage - Making blob storage easier than ever before</li>
|
||||
<li className="mb-5">
|
||||
Functions - write and deploy serverless functions that have access to your Supabase
|
||||
@@ -998,7 +997,7 @@ const Beta = (props: Props) => {
|
||||
/>
|
||||
<Container>
|
||||
<div className="sticky inset-0 z-50">
|
||||
<div className="bg-scale-1200 dark:bg-scale-300 flex items-center justify-between px-5 py-5 shadow-lg xl:px-20">
|
||||
<div className="flex items-center justify-between px-5 py-5 shadow-lg bg-scale-1200 dark:bg-scale-300 xl:px-20">
|
||||
<Link href="/">
|
||||
<a>
|
||||
<Image src={`${basePath}/images/logo-dark.png`} height={24} width={120} />
|
||||
|
||||
@@ -238,7 +238,7 @@ function RealtimePage() {
|
||||
</p>,
|
||||
]}
|
||||
// [TODO] Point to the correct docs URL
|
||||
documentation_link={'/docs/guides/realtime#broadcast'}
|
||||
documentation_link={'/docs/guides/realtime/broadcast'}
|
||||
/>
|
||||
</SectionContainer>
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 403 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 478 KiB |
+161
-154
File diff suppressed because it is too large.
Load diff
+1
-2
@@ -15,9 +15,8 @@ SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZX
|
||||
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# default user is postgres
|
||||
|
||||
############
|
||||
# API Proxy - Configuration for the Kong Reverse proxy.
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
studio:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: studio/Dockerfile
|
||||
target: dev
|
||||
ports:
|
||||
- 8082:8082
|
||||
mail:
|
||||
container_name: supabase-mail
|
||||
image: inbucket/inbucket:3.0.3
|
||||
|
||||
@@ -236,4 +236,6 @@ services:
|
||||
volumes:
|
||||
- ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/realtime.sql
|
||||
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/roles.sql
|
||||
# Must be superuser to enable pg_net extension
|
||||
- ./volumes/db/webhooks.sql:/etc/postgresql.schema.sql
|
||||
- ./volumes/db/data:/var/lib/postgresql/data
|
||||
@@ -0,0 +1,216 @@
|
||||
BEGIN;
|
||||
-- Create pg_net extension
|
||||
CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;
|
||||
-- Create supabase_functions schema
|
||||
CREATE SCHEMA if not exists supabase_functions;
|
||||
GRANT USAGE ON SCHEMA supabase_functions TO postgres;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres;
|
||||
-- supabase_functions.migrations definition
|
||||
CREATE TABLE supabase_functions.migrations (
|
||||
version text PRIMARY KEY,
|
||||
inserted_at timestamptz NOT NULL DEFAULT NOW()
|
||||
);
|
||||
-- Initial supabase_functions migration
|
||||
INSERT INTO supabase_functions.migrations (version) VALUES ('initial');
|
||||
-- supabase_functions.hooks definition
|
||||
CREATE TABLE supabase_functions.hooks (
|
||||
id bigserial PRIMARY KEY,
|
||||
hook_table_id integer NOT NULL,
|
||||
hook_name text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT NOW(),
|
||||
request_id bigint
|
||||
);
|
||||
CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id);
|
||||
CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name);
|
||||
COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
|
||||
CREATE FUNCTION supabase_functions.http_request()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
request_id bigint;
|
||||
payload jsonb;
|
||||
url text := TG_ARGV[0]::text;
|
||||
method text := TG_ARGV[1]::text;
|
||||
headers jsonb DEFAULT '{}'::jsonb;
|
||||
params jsonb DEFAULT '{}'::jsonb;
|
||||
timeout_ms integer DEFAULT 1000;
|
||||
BEGIN
|
||||
IF url IS NULL OR url = 'null' THEN
|
||||
RAISE EXCEPTION 'url argument is missing';
|
||||
END IF;
|
||||
|
||||
IF method IS NULL OR method = 'null' THEN
|
||||
RAISE EXCEPTION 'method argument is missing';
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
|
||||
headers = '{"Content-Type": "application/json"}'::jsonb;
|
||||
ELSE
|
||||
headers = TG_ARGV[2]::jsonb;
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
|
||||
params = '{}'::jsonb;
|
||||
ELSE
|
||||
params = TG_ARGV[3]::jsonb;
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
|
||||
timeout_ms = 1000;
|
||||
ELSE
|
||||
timeout_ms = TG_ARGV[4]::integer;
|
||||
END IF;
|
||||
|
||||
CASE
|
||||
WHEN method = 'GET' THEN
|
||||
SELECT http_get INTO request_id FROM net.http_get(
|
||||
url,
|
||||
params,
|
||||
headers,
|
||||
timeout_ms
|
||||
);
|
||||
WHEN method = 'POST' THEN
|
||||
payload = jsonb_build_object(
|
||||
'old_record', OLD,
|
||||
'record', NEW,
|
||||
'type', TG_OP,
|
||||
'table', TG_TABLE_NAME,
|
||||
'schema', TG_TABLE_SCHEMA
|
||||
);
|
||||
|
||||
SELECT http_post INTO request_id FROM net.http_post(
|
||||
url,
|
||||
payload,
|
||||
params,
|
||||
headers,
|
||||
timeout_ms
|
||||
);
|
||||
ELSE
|
||||
RAISE EXCEPTION 'method argument % is invalid', method;
|
||||
END CASE;
|
||||
|
||||
INSERT INTO supabase_functions.hooks
|
||||
(hook_table_id, hook_name, request_id)
|
||||
VALUES
|
||||
(TG_RELID, TG_NAME, request_id);
|
||||
|
||||
RETURN NEW;
|
||||
END
|
||||
$function$;
|
||||
-- Supabase super admin
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_roles
|
||||
WHERE rolname = 'supabase_functions_admin'
|
||||
)
|
||||
THEN
|
||||
CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
ALTER USER supabase_functions_admin SET search_path = "supabase_functions";
|
||||
ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin;
|
||||
ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin;
|
||||
ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin;
|
||||
GRANT supabase_functions_admin TO postgres;
|
||||
-- Remove unused supabase_pg_net_admin role
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_roles
|
||||
WHERE rolname = 'supabase_pg_net_admin'
|
||||
)
|
||||
THEN
|
||||
REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin;
|
||||
DROP OWNED BY supabase_pg_net_admin;
|
||||
DROP ROLE supabase_pg_net_admin;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
-- pg_net grants when extension is already enabled
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_extension
|
||||
WHERE extname = 'pg_net'
|
||||
)
|
||||
THEN
|
||||
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_collect_response(request_id bigint, async boolean) SECURITY DEFINER;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
ALTER function net.http_collect_response(request_id bigint, async boolean) SET search_path = net;
|
||||
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION net.http_collect_response(request_id bigint, async boolean) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres;
|
||||
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres;
|
||||
GRANT EXECUTE ON FUNCTION net.http_collect_response(request_id bigint, async boolean) TO supabase_functions_admin, postgres;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
-- Event trigger for pg_net
|
||||
CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
|
||||
RETURNS event_trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_event_trigger_ddl_commands() AS ev
|
||||
JOIN pg_extension AS ext
|
||||
ON ev.objid = ext.oid
|
||||
WHERE ext.extname = 'pg_net'
|
||||
)
|
||||
THEN
|
||||
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_collect_response(request_id bigint, async boolean) SECURITY DEFINER;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
ALTER function net.http_collect_response(request_id bigint, async boolean) SET search_path = net;
|
||||
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION net.http_collect_response(request_id bigint, async boolean) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres;
|
||||
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres;
|
||||
GRANT EXECUTE ON FUNCTION net.http_collect_response(request_id bigint, async boolean) TO supabase_functions_admin, postgres;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net';
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_event_trigger
|
||||
WHERE evtname = 'issue_pg_net_access'
|
||||
) THEN
|
||||
CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION')
|
||||
EXECUTE PROCEDURE extensions.grant_pg_net_access();
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants');
|
||||
ALTER function supabase_functions.http_request() SECURITY DEFINER;
|
||||
ALTER function supabase_functions.http_request() SET search_path = supabase_functions;
|
||||
REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres;
|
||||
COMMIT;
|
||||
+140
-18
@@ -9,13 +9,16 @@
|
||||
|
||||
[Supabase](https://supabase.com) 是 Firebase 的開放原始碼替代方案。我們以企業等級的開放原始碼工具,打造 Firebase 的功能。
|
||||
|
||||
- [x] Postgres 資料庫託管
|
||||
- [x] 即時訂閱
|
||||
- [x] 認證與授權
|
||||
- [x] Postgres 資料庫託管 [Docs](https://supabase.com/docs/guides/database)
|
||||
- [x] 認證與授權 [Docs](https://supabase.com/docs/guides/auth)
|
||||
- [x] 自動產生的 API
|
||||
- [x] 儀表版
|
||||
- [x] Storage
|
||||
- [x] REST. [Docs](https://supabase.com/docs/guides/api#rest-api)
|
||||
- [x] 實時訂閱. [Docs](https://supabase.com/docs/guides/api#realtime-api)
|
||||
- [x] GraphQL (Beta). [Docs](https://supabase.com/docs/guides/api#graphql-api)
|
||||
- [x] Functions
|
||||
- [x] 數據庫 Functions. [Docs](https://supabase.com/docs/guides/database/functions)
|
||||
- [x] 邊緣 Functions [Docs](https://supabase.com/docs/guides/functions)
|
||||
- [x] 儀表版
|
||||
|
||||

|
||||
|
||||
@@ -23,11 +26,14 @@
|
||||
|
||||
完整的說明文件請見:[supabase.com/docs](https://supabase.com/docs)
|
||||
|
||||
如何貢獻請見:[Getting Started](./DEVELOPERS.md)
|
||||
|
||||
## 社群與支援
|
||||
|
||||
- [社群論壇](https://github.com/supabase/supabase/discussions)。適合:打造、討論資料庫最佳作法的協助。
|
||||
- [GitHub Issues](https://github.com/supabase/supabase/issues)。適合:在使用 Supabase 的時候遇到臭蟲和錯誤。
|
||||
- [電子郵件支援](https://supabase.com/docs/support#business-support)。適合:資料庫或基礎建設遇到的問題。
|
||||
- [Discord](https://discord.supabase.com)。適合:分享你的應用程式並與社群互動。
|
||||
|
||||
## 狀態
|
||||
|
||||
@@ -48,7 +54,7 @@ Supabase 是一套開放原始碼工具的組合包。我們以企業等級的
|
||||
|
||||
**目前架構**
|
||||
|
||||
Supabase 是[託管平台](https://app.supabase.com)。只要註冊,不必安裝任何東西,就可以開始使用 Supabase。目前還在打造本地端開發體驗:連同平台的穩定度,這是我們專注的核心。
|
||||
Supabase 是[託管平台](https://app.supabase.com)。只要註冊,不必安裝任何東西,就可以開始使用 Supabase。你也可以[自託管](https://supabase.com/docs/guides/hosting/overview)和[本地端開發](https://supabase.com/docs/guides/local-development)。
|
||||
|
||||

|
||||
|
||||
@@ -64,18 +70,134 @@ Supabase 是[託管平台](https://app.supabase.com)。只要註冊,不必安
|
||||
|
||||
客戶端 Library 是模組化的。每個子 Library 都是單一外部系統的獨立實作。這是支援既有工具的方法之一。
|
||||
|
||||
- **`supabase-{lang}`**: Combines libraries and adds enrichments.
|
||||
- `postgrest-{lang}`: 銜接 [PostgREST](https://github.com/postgrest/postgrest) 的客戶端 Library
|
||||
- `realtime-{lang}`: 銜接 [Realtime](https://github.com/supabase/realtime) 的客戶端 Library
|
||||
- `gotrue-{lang}`: 銜接 [GoTrue](https://github.com/netlify/gotrue) 的客戶端 Library
|
||||
|
||||
| Repo | 官方 | 社群 |
|
||||
| --------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`supabase-{lang}`** | [`JS`](https://github.com/supabase/supabase-js) | [`C#`](https://github.com/supabase/supabase-csharp) \| [`Flutter`](https://github.com/supabase/supabase-flutter) \| [`Python`](https://github.com/supabase/supabase-py) \| `Rust` \| [`Ruby`](https://github.com/supabase/supabase-rb) \| `Go` |
|
||||
| `postgrest-{lang}` | [`JS`](https://github.com/supabase/postgrest-js) | [`C#`](https://github.com/supabase/postgrest-csharp) \| [`Dart`](https://github.com/supabase/postgrest-dart) \| [`Python`](https://github.com/supabase/postgrest-py) \| [`Rust`](https://github.com/supabase/postgrest-rs) \| [`Ruby`](https://github.com/supabase/postgrest-rb) \| [`Go`](https://github.com/supabase/postgrest-go) |
|
||||
| `realtime-{lang}` | [`JS`](https://github.com/supabase/realtime-js) | [`C#`](https://github.com/supabase/realtime-csharp) \| [`Dart`](https://github.com/supabase/realtime-dart) \| [`Python`](https://github.com/supabase/realtime-py) \| `Rust` \| `Ruby` \| `Go` |
|
||||
| `gotrue-{lang}` | [`JS`](https://github.com/supabase/gotrue-js) | [`C#`](https://github.com/supabase/gotrue-csharp) \| [`Dart`](https://github.com/supabase/gotrue-dart) \| [`Python`](https://github.com/supabase/gotrue-py) \| `Rust` \| `Ruby` \| `Go` |
|
||||
|
||||
<table style="table-layout:fixed; white-space: nowrap;">
|
||||
<tr>
|
||||
<th>語言</th>
|
||||
<th>客戶端</th>
|
||||
<th colspan="5">精選客戶端(包含在 Supabase 客戶端)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Supabase</th>
|
||||
<th><a href="https://github.com/postgrest/postgrest" target="_blank" rel="noopener noreferrer">PostgREST</a></th>
|
||||
<th><a href="https://github.com/supabase/gotrue" target="_blank" rel="noopener noreferrer">GoTrue</a></th>
|
||||
<th><a href="https://github.com/supabase/realtime" target="_blank" rel="noopener noreferrer">Realtime</a></th>
|
||||
<th><a href="https://github.com/supabase/storage-api" target="_blank" rel="noopener noreferrer">Storage</a></th>
|
||||
<th>Functions</th>
|
||||
</tr>
|
||||
<!-- TEMPLATE FOR NEW ROW -->
|
||||
<!-- START ROW
|
||||
<tr>
|
||||
<td>lang</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-lang" target="_blank" rel="noopener noreferrer">supabase-lang</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-lang" target="_blank" rel="noopener noreferrer">postgrest-lang</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-lang" target="_blank" rel="noopener noreferrer">gotrue-lang</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-lang" target="_blank" rel="noopener noreferrer">realtime-lang</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-lang" target="_blank" rel="noopener noreferrer">storage-lang</a></td>
|
||||
</tr>
|
||||
END ROW -->
|
||||
<th colspan="7">⚡️ 官方 ⚡️</th>
|
||||
<tr>
|
||||
<td>JavaScript (TypeScript)</td>
|
||||
<td><a href="https://github.com/supabase/supabase-js" target="_blank" rel="noopener noreferrer">supabase-js</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-js" target="_blank" rel="noopener noreferrer">postgrest-js</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-js" target="_blank" rel="noopener noreferrer">gotrue-js</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-js" target="_blank" rel="noopener noreferrer">realtime-js</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-js" target="_blank" rel="noopener noreferrer">storage-js</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-js" target="_blank" rel="noopener noreferrer">functions-js</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Flutter</td>
|
||||
<td><a href="https://github.com/supabase/supabase-flutter" target="_blank" rel="noopener noreferrer">supabase-flutter</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-dart" target="_blank" rel="noopener noreferrer">realtime-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
|
||||
</tr>
|
||||
<th colspan="7">💚 社群 💚</th>
|
||||
<tr>
|
||||
<td>C#</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-csharp" target="_blank" rel="noopener noreferrer">supabase-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-csharp" target="_blank" rel="noopener noreferrer">postgrest-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-csharp" target="_blank" rel="noopener noreferrer">gotrue-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-csharp" target="_blank" rel="noopener noreferrer">realtime-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-csharp" target="_blank" rel="noopener noreferrer">storage-csharp</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-csharp" target="_blank" rel="noopener noreferrer">functions-csharp</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Go</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-go" target="_blank" rel="noopener noreferrer">postgrest-go</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-go" target="_blank" rel="noopener noreferrer">gotrue-go</a></td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/storage-go" target="_blank" rel="noopener noreferrer">storage-go</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-go" target="_blank" rel="noopener noreferrer">functions-go</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Java</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-java" target="_blank" rel="noopener noreferrer">gotrue-java</a></td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/storage-java" target="_blank" rel="noopener noreferrer">storage-java</a></td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kotlin</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt" target="_blank" rel="noopener noreferrer">supabase-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-kt" target="_blank" rel="noopener noreferrer">postgrest-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-kt" target="_blank" rel="noopener noreferrer">gotrue-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Realtime" target="_blank" rel="noopener noreferrer">realtime-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Storage" target="_blank" rel="noopener noreferrer">storage-kt</a></td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Functions" target="_blank" rel="noopener noreferrer">functions-kt</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Python</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-py" target="_blank" rel="noopener noreferrer">supabase-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-py" target="_blank" rel="noopener noreferrer">postgrest-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-py" target="_blank" rel="noopener noreferrer">gotrue-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-py" target="_blank" rel="noopener noreferrer">realtime-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-py" target="_blank" rel="noopener noreferrer">storage-py</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-py" target="_blank" rel="noopener noreferrer">functions-py</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ruby</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-rb" target="_blank" rel="noopener noreferrer">supabase-rb</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-rb" target="_blank" rel="noopener noreferrer">postgrest-rb</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rust</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-rs" target="_blank" rel="noopener noreferrer">postgrest-rs</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swift</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-swift" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-swift" target="_blank" rel="noopener noreferrer">gotrue-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-swift" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-swift" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-swift" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Godot Engine (GDScript)</td>
|
||||
<td><a href="https://github.com/supabase-community/godot-engine.supabase" target="_blank" rel="noopener noreferrer">supabase-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-gdscript" target="_blank" rel="noopener noreferrer">postgrest-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-gdscript" target="_blank" rel="noopener noreferrer">gotrue-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-gdscript" target="_blank" rel="noopener noreferrer">realtime-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-gdscript" target="_blank" rel="noopener noreferrer">storage-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-gdscript" target="_blank" rel="noopener noreferrer">functions-gdscript</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--- Remove this list if you're traslating to another language, it's hard to keep updated across multiple files-->
|
||||
<!--- Keep only the link to the list of translation files-->
|
||||
|
||||
|
||||
Generated
+1069
-135
File diff suppressed because it is too large.
Load diff
@@ -325,5 +325,16 @@ module.exports = ui({
|
||||
// height: ['hover'],
|
||||
// },
|
||||
// },
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
function ({ addUtilities, addVariant }) {
|
||||
addUtilities({
|
||||
// prose (tailwind typography) helpers
|
||||
// useful for removing margins in prose styled sections
|
||||
'.prose--remove-p-margin p': {
|
||||
margin: '0',
|
||||
},
|
||||
})
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -80,6 +80,7 @@ export function RightSlot({ children }: any) {
|
||||
}
|
||||
|
||||
interface ItemProps {
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
disabled?: boolean
|
||||
@@ -87,11 +88,15 @@ interface ItemProps {
|
||||
rightSlot?: React.ReactNode
|
||||
}
|
||||
|
||||
export function Item({ children, icon, disabled, onClick, rightSlot }: ItemProps) {
|
||||
export function Item({ className, children, icon, disabled, onClick, rightSlot }: ItemProps) {
|
||||
let __styles = styleHandler('dropdown')
|
||||
|
||||
return (
|
||||
<RadixDropdown.Item className={__styles.item} disabled={disabled} onSelect={onClick}>
|
||||
<RadixDropdown.Item
|
||||
className={`${__styles.item} ${className}`}
|
||||
disabled={disabled}
|
||||
onSelect={onClick}
|
||||
>
|
||||
{icon && icon}
|
||||
<span>{children}</span>
|
||||
</RadixDropdown.Item>
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import React, { ComponentProps } from 'react'
|
||||
import {
|
||||
Toaster as HotToaster,
|
||||
toast as hotToast,
|
||||
resolveValue,
|
||||
} from 'react-hot-toast'
|
||||
import {
|
||||
Toast as HotToastProps,
|
||||
ToastType,
|
||||
} from 'react-hot-toast/dist/core/types'
|
||||
import { Toaster as HotToaster, toast as hotToast, resolveValue } from 'react-hot-toast'
|
||||
import { Toast as HotToastProps, ToastType } from 'react-hot-toast/dist/core/types'
|
||||
import * as Portal from '@radix-ui/react-portal'
|
||||
import { IconCheck } from '../Icon/icons/IconCheck'
|
||||
import { IconLoader } from '../Icon/icons/IconLoader'
|
||||
@@ -30,10 +23,7 @@ export interface ToastProps extends Partial<HotToastProps> {
|
||||
width?: 'xs' | 'sm' | 'md'
|
||||
}
|
||||
|
||||
function Message({
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof Typography.Text>) {
|
||||
function Message({ children, ...props }: ComponentProps<typeof Typography.Text>) {
|
||||
return (
|
||||
<Typography.Text className={ToastStyles['sbui-toast-message']} {...props}>
|
||||
{children}
|
||||
@@ -41,15 +31,9 @@ function Message({
|
||||
)
|
||||
}
|
||||
|
||||
function Description({
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof Typography.Text>) {
|
||||
function Description({ children, ...props }: ComponentProps<typeof Typography.Text>) {
|
||||
return (
|
||||
<Typography.Text
|
||||
className={ToastStyles['sbui-toast-description']}
|
||||
{...props}
|
||||
>
|
||||
<Typography.Text className={ToastStyles['sbui-toast-description']} {...props}>
|
||||
{children}
|
||||
</Typography.Text>
|
||||
)
|
||||
@@ -100,18 +84,10 @@ function Toast({
|
||||
}
|
||||
|
||||
const _message =
|
||||
typeof message === 'string' ? (
|
||||
<Message>{message}</Message>
|
||||
) : (
|
||||
resolveValue(message, rest)
|
||||
)
|
||||
typeof message === 'string' ? <Message>{message}</Message> : resolveValue(message, rest)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${containerClasses.join(' ')} ${
|
||||
visible ? 'animate-enter' : 'animate-leave'
|
||||
}`}
|
||||
>
|
||||
<div className={`${containerClasses.join(' ')} ${visible ? 'animate-enter' : 'animate-leave'}`}>
|
||||
<div>
|
||||
<Typography.Text className={ToastStyles['sbui-toast-icon-container']}>
|
||||
{type === 'loading' ? (
|
||||
@@ -129,28 +105,21 @@ function Toast({
|
||||
{_message}
|
||||
{description && <Description>{description}</Description>}
|
||||
</div>
|
||||
{actions && (
|
||||
<div className={ToastStyles['sbui-toast-details__actions']}>
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
{actions && <div className={ToastStyles['sbui-toast-details__actions']}>{actions}</div>}
|
||||
</div>
|
||||
{closable && (
|
||||
<div className={ToastStyles['sbui-toast-close-container']}>
|
||||
<button
|
||||
aria-label="Close alert"
|
||||
className={closeButtonClasses.join(' ')}
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
hotToast.dismiss(id)
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<IconX
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
size="small"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<IconX className="h-5 w-5" aria-hidden="true" size="small" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -200,8 +169,7 @@ type ToastOptions = Partial<
|
||||
>
|
||||
|
||||
export function toast(message: string, opts?: ToastOptions) {
|
||||
const { description, closable, actions, actionsPosition, type, ...rest } =
|
||||
opts || {}
|
||||
const { description, closable, actions, actionsPosition, type, ...rest } = opts || {}
|
||||
|
||||
return hotToast(
|
||||
({ message: _m, type: _t, ...t }) => (
|
||||
@@ -219,16 +187,13 @@ export function toast(message: string, opts?: ToastOptions) {
|
||||
)
|
||||
}
|
||||
|
||||
const createToastType = (type: ToastType) => (
|
||||
message: string,
|
||||
opts?: Omit<ToastOptions, 'type'>
|
||||
) => toast(message, { ...opts, type })
|
||||
const createToastType = (type: ToastType) => (message: string, opts?: Omit<ToastOptions, 'type'>) =>
|
||||
toast(message, { ...opts, type })
|
||||
|
||||
toast.success = createToastType('success')
|
||||
toast.error = createToastType('error')
|
||||
toast.loading = createToastType('loading')
|
||||
toast.promise = (...args: Parameters<typeof hotToast.promise>) =>
|
||||
hotToast.promise(...args)
|
||||
toast.promise = (...args: Parameters<typeof hotToast.promise>) => hotToast.promise(...args)
|
||||
|
||||
Toast.Toaster = Toaster
|
||||
Toast.toast = toast
|
||||
|
||||
@@ -10,14 +10,17 @@
|
||||
"id": "installing",
|
||||
"slug": "installing",
|
||||
"type": "markdown",
|
||||
"excludes": ["reference_javascript_v1", "reference_dart_v0", "reference_python_v2"]
|
||||
"excludes": [
|
||||
"reference_javascript_v1",
|
||||
"reference_dart_v0",
|
||||
"reference_python_v2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Initializing",
|
||||
"id": "initializing",
|
||||
"slug": "initializing",
|
||||
"type": "markdown",
|
||||
"libs": ["csharp"]
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"title": "Typescript Support",
|
||||
@@ -355,6 +358,14 @@
|
||||
"product": "database",
|
||||
"parent": "modifiers",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"id": "returns",
|
||||
"title": "Override type of successful response",
|
||||
"slug": "db-returns",
|
||||
"product": "database",
|
||||
"parent": "modifiers",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -367,7 +378,6 @@
|
||||
"id": "auth-api",
|
||||
"title": "Overview",
|
||||
"slug": "auth-api",
|
||||
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
@@ -570,7 +580,6 @@
|
||||
"id": "admin-api",
|
||||
"title": "Auth Admin",
|
||||
"slug": "admin-api",
|
||||
|
||||
"type": "function",
|
||||
"items": [
|
||||
{
|
||||
@@ -704,7 +713,6 @@
|
||||
"product": "realtime",
|
||||
"type": "function"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "remove-channel",
|
||||
"title": "Unsubscribe from a channel",
|
||||
@@ -847,7 +855,10 @@
|
||||
},
|
||||
{
|
||||
"title": "Misc",
|
||||
"excludes": ["reference_dart_v1", "reference_dart_v0"],
|
||||
"excludes": [
|
||||
"reference_dart_v1",
|
||||
"reference_dart_v0"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"title": "Release Notes",
|
||||
@@ -858,4 +869,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -16,6 +16,104 @@ info:
|
||||
version: '0.8.0'
|
||||
|
||||
functions:
|
||||
- id: initializing
|
||||
title: Initializing
|
||||
description: |
|
||||
Initializing a new client is pretty straightforward. Find your project url and public key from the
|
||||
admin panel and pass it into your client initialization function.
|
||||
|
||||
`supabase-csharp` is heavily dependent on Models deriving from `BaseModel`. To interact with the API, one must have the associated model (see example) specified.
|
||||
|
||||
Leverage `Table`, `PrimaryKey`, and `Column` attributes to specify names of classes/properties that are different from their C# Versions.
|
||||
|
||||
examples:
|
||||
- id: csharp-init-standard
|
||||
name: Standard
|
||||
code: |
|
||||
```c#
|
||||
var url = Environment.GetEnvironmentVariable("SUPABASE_URL");
|
||||
var key = Environment.GetEnvironmentVariable("SUPABASE_KEY");
|
||||
|
||||
var options = new Supabase.SupabaseOptions
|
||||
{
|
||||
AutoConnectRealtime = true
|
||||
};
|
||||
|
||||
var supabase = new Supabase.Client(url, key, options);
|
||||
await supabase.InitializeAsync();
|
||||
```
|
||||
- id: csharp-init-maui
|
||||
name: Dependency Injection (Maui-like)
|
||||
code: |
|
||||
```c#
|
||||
public static MauiApp CreateMauiApp()
|
||||
{
|
||||
// ...
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
|
||||
var url = Environment.GetEnvironmentVariable("SUPABASE_URL");
|
||||
var key = Environment.GetEnvironmentVariable("SUPABASE_KEY");
|
||||
var options = new SupabaseOptions
|
||||
{
|
||||
AutoRefreshToken = true,
|
||||
AutoConnectRealtime = true,
|
||||
SessionHandler = new SupabaseSessionHandler()
|
||||
};
|
||||
|
||||
// Note the creation as a singleton.
|
||||
builder.Services.AddSingleton(provider => new Supabase.Client(url, key, options));
|
||||
}
|
||||
```
|
||||
- id: csharp-init-showing-models
|
||||
name: With Models Example
|
||||
code: |
|
||||
```c#
|
||||
// Given the following Model representing the Supabase Database (Message.cs)
|
||||
[Table("messages")]
|
||||
public class Message : BaseModel
|
||||
{
|
||||
[PrimaryKey("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("username")]
|
||||
public string UserName { get; set; }
|
||||
|
||||
[Column("channel_id")]
|
||||
public int ChannelId { get; set; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is Message message &&
|
||||
Id == message.Id;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Id);
|
||||
}
|
||||
}
|
||||
|
||||
void Initialize()
|
||||
{
|
||||
// Get All Messages
|
||||
var response = await client.Table<Message>().Get();
|
||||
List<Message> models = response.Models;
|
||||
|
||||
// Insert
|
||||
var newMessage = new Message { UserName = "acupofjose", ChannelId = 1 };
|
||||
await client.Table<Message>().Insert();
|
||||
|
||||
// Update
|
||||
var model = response.Models.First();
|
||||
model.UserName = "elrhomariyounes";
|
||||
await model.Update();
|
||||
|
||||
// Delete
|
||||
await response.Models.Last().Delete();
|
||||
|
||||
// etc.
|
||||
}
|
||||
```
|
||||
- id: sign-up
|
||||
title: 'SignUp()'
|
||||
description: |
|
||||
|
||||
+75
-5
@@ -66,9 +66,9 @@ functions:
|
||||
db: { schema: 'other_schema' }
|
||||
})
|
||||
```
|
||||
|
||||
description: |
|
||||
By default the API server points to the `public` schema. You can enable other database schemas within the Dashboard.
|
||||
Go to `Settings > API > Schema` and add the schema which you want to expose to the API.
|
||||
Go to [Settings > API > Exposed schemas](https://app.supabase.com/project/_/settings/api) and add the schema which you want to expose to the API.
|
||||
|
||||
Note: each client connection can only access a single schema, so the code above can access the `other_schema` schema but cannot access the `public` schema.
|
||||
- id: custom-fetch-implementation
|
||||
@@ -81,7 +81,7 @@ functions:
|
||||
global: { fetch: fetch.bind(globalThis) }
|
||||
})
|
||||
```
|
||||
|
||||
description: |
|
||||
`supabase-js` uses the [`cross-fetch`](https://www.npmjs.com/package/cross-fetch) library to make HTTP requests,
|
||||
but an alternative `fetch` implementation can be provided as an option.
|
||||
This is most useful in environments where `cross-fetch` is not compatible (for instance Cloudflare Workers).
|
||||
@@ -101,7 +101,7 @@ functions:
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
description: |
|
||||
For React Native we recommend using `AsyncStorage` as the storage implementation for Supabase Auth.
|
||||
- id: auth-api
|
||||
title: 'Overview'
|
||||
@@ -1194,6 +1194,56 @@ functions:
|
||||
table returns `[]` or `null` but the parent table is not filtered out.
|
||||
If you want to filter out the parent table rows, use the `!inner` hint
|
||||
hideCodeBlock: true
|
||||
- id: querying-foreign-table-with-count
|
||||
name: Querying foreign table with count
|
||||
code: |
|
||||
```ts
|
||||
const { data, error } = await supabase
|
||||
.from('countries')
|
||||
.select(`*, cities(count)`)
|
||||
```
|
||||
data:
|
||||
sql: |
|
||||
```sql
|
||||
create table countries (
|
||||
"id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
|
||||
"name" text
|
||||
);
|
||||
|
||||
create table cities (
|
||||
"id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
|
||||
"name" text,
|
||||
"country_id" "uuid" references public.countries on delete cascade
|
||||
);
|
||||
|
||||
with country as (
|
||||
insert into countries (name)
|
||||
values ('united kingdom') returning id
|
||||
)
|
||||
insert into cities (name, country_id) values
|
||||
('London', (select id from country)),
|
||||
('Manchester', (select id from country)),
|
||||
('Liverpool', (select id from country)),
|
||||
('Bristol', (select id from country));
|
||||
```
|
||||
response: |
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "693694e7-d993-4360-a6d7-6294e325d9b6",
|
||||
"name": "United Kingdom",
|
||||
"cities": [
|
||||
{
|
||||
"count": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
description: |
|
||||
You can get the number of rows in a related table by using the
|
||||
**count** property.
|
||||
hideCodeBlock: true
|
||||
- id: querying-with-count-option
|
||||
name: Querying with count option
|
||||
code: |
|
||||
@@ -3845,6 +3895,25 @@ functions:
|
||||
By default, the data is returned in JSON format, but can also be returned as Comma Separated Values.
|
||||
hideCodeBlock: true
|
||||
isSpotlight: true
|
||||
- id: returns
|
||||
$ref: '@supabase/postgrest-js.PostgrestTransformBuilder.returns'
|
||||
title: returns()
|
||||
examples:
|
||||
- id: override-type-of-successful-response
|
||||
name: Override type of successful response
|
||||
code: |
|
||||
```ts
|
||||
const { data } = await supabase
|
||||
.from('countries')
|
||||
.select()
|
||||
.returns<MyType>()
|
||||
```
|
||||
response: |
|
||||
```
|
||||
let x: typeof data // MyType | null
|
||||
```
|
||||
hideCodeBlock: true
|
||||
isSpotlight: true
|
||||
|
||||
# NOTE: Not available on currently deployed PostgREST
|
||||
# db.geojson():
|
||||
@@ -4371,7 +4440,8 @@ functions:
|
||||
.from('avatars')
|
||||
.update('public/avatar1.png', avatarFile, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
// Overwrite file if it exists
|
||||
upsert: true
|
||||
})
|
||||
```
|
||||
- id: update-file-using-arraybuffer-from-base64-file-data
|
||||
|
||||
Loaded 100 of 154 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user