Merge pull request #13413 from supabase/master

Prod deploy dashboard
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-04-04 14:34:07 +08:00
commit 44993870f5
301 files changed
+22808 -14872

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@ You need to install and configure the following dependencies on your machine to
- [Git](http://git-scm.com/)
- [Node.js v16.x (LTS)](http://nodejs.org)
- [npm](https://www.npmjs.com/) version 7+ or [Yarn](https://yarnpkg.com/)
- [npm](https://www.npmjs.com/) version 8.x.x or [Yarn](https://yarnpkg.com/)
## Local development
+1 -3
View File
@@ -3,13 +3,11 @@ import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FC, useEffect, useState } from 'react'
import { IconCommand, IconMenu, IconMoon, IconSearch, IconSun, Listbox } from 'ui'
import { IconCommand, IconMenu, IconMoon, IconSearch, IconSun, Listbox, SearchButton } from 'ui'
import { REFERENCES } from './Navigation.constants'
import { getPageType } from '~/lib/helpers'
import SearchButton from '../Search/SearchButton'
const NavBar: FC = () => {
const { isDarkMode, toggleTheme } = useTheme()
const [mounted, setMounted] = useState(false)
@@ -80,6 +80,7 @@ export const menuItems: NavMenu = {
label: 'Auth',
items: [
{ name: 'Overview', url: '/guides/auth', items: [] },
{ name: 'Quickstarts', url: '/guides/auth/quickstarts/react', items: [] },
{
name: 'Authentication',
url: undefined,
@@ -129,9 +130,9 @@ export const menuItems: NavMenu = {
items: [
{ name: 'Overview', url: '/guides/auth/auth-helpers', items: [] },
{ name: 'Auth UI', url: '/guides/auth/auth-helpers/auth-ui', items: [] },
{ name: 'Next.js', url: '/guides/auth/auth-helpers/nextjs', items: [] },
{ name: 'Next.js (pages)', url: '/guides/auth/auth-helpers/nextjs', items: [] },
{
name: 'Next.js Server Components',
name: 'Next.js (app)',
url: '/guides/auth/auth-helpers/nextjs-server-components',
items: [],
},
@@ -174,14 +175,6 @@ export const menuItems: NavMenu = {
{ name: 'Database Webhooks', url: '/guides/database/webhooks', items: [] },
{ name: 'Full Text Search', url: '/guides/database/full-text-search', items: [] },
{ name: 'Database Testing', url: '/guides/database/testing', items: [] },
{
name: 'Serverless APIs',
url: undefined,
items: [
{ name: 'Overview', url: '/guides/database/api', items: [] },
{ name: 'Generating Types', url: '/guides/database/api/generating-types', items: [] },
],
},
{
name: 'Extensions',
url: undefined,
@@ -228,6 +221,13 @@ export const menuItems: NavMenu = {
},
],
},
{
label: 'Serverless APIs',
items: [
{ name: 'Overview', url: '/guides/api', items: [] },
{ name: 'Generating Types', url: '/guides/api/generating-types', items: [] },
],
},
{
label: 'Edge Functions',
items: [
@@ -388,7 +388,6 @@ export const menuItems: NavMenu = {
url: undefined,
items: [
{ name: 'Appsmith', url: '/guides/integrations/appsmith', items: [] },
{ name: 'Dashibase', url: '/guides/integrations/dashibase', items: [] },
{ name: 'DhiWise', url: '/guides/integrations/dhiwise', items: [] },
{ name: 'Directus', url: '/guides/integrations/directus', items: [] },
{ name: 'Draftbit', url: '/guides/integrations/draftbit', items: [] },
@@ -27,6 +27,12 @@ const home = [
href: '/guides/database',
level: 'database',
},
{
label: 'Serverless APIs',
icon: '/img/icons/menu/reference',
href: '/guides/api',
level: 'api',
},
{
label: 'Auth',
icon: '/img/icons/menu/auth',
@@ -131,7 +131,7 @@ export const gettingstarted = {
}
export const cli = {
label: 'CLI',
title: 'CLI',
items: [
{ name: 'Overview', url: '/guides/cli' },
{ name: 'Local Development', url: '/guides/cli/local-development' },
@@ -253,12 +253,16 @@ export const PhoneLoginsItems = [
export const auth = {
icon: 'auth',
label: 'Auth',
title: 'Auth',
items: [
{
name: 'Overview',
url: '/guides/auth',
},
{
name: 'Quickstarts',
items: [{ name: 'Auth with React', url: '/guides/auth/quickstarts/react', items: [] }],
},
{
name: 'Authentication',
url: undefined,
@@ -305,9 +309,9 @@ export const auth = {
items: [
{ name: 'Overview', url: '/guides/auth/auth-helpers' },
{ name: 'Auth UI', url: '/guides/auth/auth-helpers/auth-ui' },
{ name: 'Next.js', url: '/guides/auth/auth-helpers/nextjs' },
{ name: 'Next.js (pages)', url: '/guides/auth/auth-helpers/nextjs' },
{
name: 'Next.js Server Components',
name: 'Next.js (app)',
url: '/guides/auth/auth-helpers/nextjs-server-components',
},
{ name: 'Remix', url: '/guides/auth/auth-helpers/remix' },
@@ -339,7 +343,7 @@ export const auth = {
export const database = {
icon: 'database',
label: 'Database',
title: 'Database',
url: '/guides/database',
items: [
{ name: 'Database Connections', url: '/guides/database/connecting-to-postgres' },
@@ -348,14 +352,6 @@ export const database = {
{ name: 'Database Webhooks', url: '/guides/database/webhooks' },
{ name: 'Full Text Search', url: '/guides/database/full-text-search' },
{ name: 'Database Testing', url: '/guides/database/testing' },
{
name: 'Serverless APIs',
url: undefined,
items: [
{ name: 'Overview', url: '/guides/database/api' },
{ name: 'Generating Types', url: '/guides/database/api/generating-types' },
],
},
{
name: 'Extensions',
url: undefined,
@@ -496,9 +492,47 @@ export const database = {
],
}
export const api = {
icon: 'reference',
title: 'Serverless APIs',
url: '/guides/api',
items: [
{ name: 'Overview', url: '/guides/api', items: [] },
{ name: 'Quickstart', url: '/guides/api/quickstart', items: [] },
{
name: 'Guides',
url: '/guides/api',
items: [
{ name: 'Creating API routes', url: '/guides/api/creating-routes', items: [] },
{ name: 'How API Keys work', url: '/guides/api/api-keys', items: [] },
{ name: 'Securing your API', url: '/guides/api/securing-your-api', items: [] },
{
name: 'Querying joins and nested tables',
url: '/guides/api/joins-and-nesting',
items: [],
},
],
},
{
name: 'REST & REALTIME',
url: undefined,
items: [
{ name: 'Auto-generated Docs', url: '/guides/api/rest/auto-generated-docs', items: [] },
{ name: 'Client Libraries', url: '/guides/api/rest/client-libs', items: [] },
{ name: 'Generating Types', url: '/guides/api/rest/generating-types', items: [] },
],
},
{
name: 'GRAPHQL',
url: undefined,
items: [{ name: 'GraphiQL Documentation', url: '/guides/api/graphql/graphiql', items: [] }],
},
],
}
export const functions = {
icon: 'functions',
label: 'Edge Functions',
title: 'Edge Functions',
url: '/guides/functions',
items: [
{
@@ -565,7 +599,7 @@ export const functions = {
export const realtime = {
icon: 'realtime',
label: 'Realtime',
title: 'Realtime',
url: '/guides/realtime',
items: [
{
@@ -628,7 +662,7 @@ export const realtime = {
export const storage = {
icon: 'storage',
label: 'Storage',
title: 'Storage',
url: '/guides/storage',
items: [
{ name: 'Overview', url: '/guides/storage' },
@@ -660,7 +694,7 @@ export const supabase_cli = {
export const platform = {
icon: 'platform',
label: 'Platform',
title: 'Platform',
url: '/guides/platform',
items: [
{
@@ -732,7 +766,7 @@ export const platform = {
export const resources = {
icon: 'resources',
label: 'Resources',
title: 'Resources',
url: '/guides/resources',
items: [
{ name: 'Examples', url: '/guides/resources/examples' },
@@ -798,7 +832,7 @@ export const self_hosting = {
}
export const migrate = {
label: 'Migrate to Supabase',
title: 'Migrate to Supabase',
url: '/guides/migrate',
items: [
{ name: 'Firebase Auth', url: '/guides/migrations/firebase-auth' },
@@ -811,7 +845,7 @@ export const migrate = {
export const integrations = {
icon: 'integrations',
label: 'Integrations',
title: 'Integrations',
url: '/guides/integrations',
items: [
{ name: 'Overview', url: '/guides/integrations/integrations' },
@@ -856,7 +890,6 @@ export const integrations = {
url: undefined,
items: [
{ name: 'Appsmith', url: '/guides/integrations/appsmith' },
{ name: 'Dashibase', url: '/guides/integrations/dashibase' },
{ name: 'DhiWise', url: '/guides/integrations/dhiwise' },
{ name: 'Directus', url: '/guides/integrations/directus' },
{ name: 'Draftbit', url: '/guides/integrations/draftbit' },
@@ -85,6 +85,9 @@ const NavigationMenu = () => {
case url.includes(`/docs/guides/database`) && url:
menuState.setMenuLevelId('database')
break
case url.includes(`/docs/guides/api`) && url:
menuState.setMenuLevelId('api')
break
case url.includes(`/docs/guides/auth`) && url:
menuState.setMenuLevelId('auth')
break
@@ -174,6 +177,7 @@ const NavigationMenu = () => {
const isHomeActive = 'home' === level
const isGettingStartedActive = 'gettingstarted' === level
const isDatabaseActive = 'database' === level
const isApiActive = 'api' === level
const isAuthActive = 'auth' === level
const isFunctionsActive = 'functions' === level
const isRealtimeActive = 'realtime' === level
@@ -203,6 +207,7 @@ const NavigationMenu = () => {
<NavigationMenuHome active={isHomeActive} />
<NavigationMenuGuideList id={'gettingstarted'} active={isGettingStartedActive} />
<NavigationMenuGuideList id={'database'} active={isDatabaseActive} />
<NavigationMenuGuideList id={'api'} active={isApiActive} />
<NavigationMenuGuideList id={'auth'} active={isAuthActive} />
<NavigationMenuGuideList id={'functions'} active={isFunctionsActive} />
<NavigationMenuGuideList
@@ -21,7 +21,11 @@ const HeaderImage = React.memo(function HeaderImage(props: any) {
)
})
const HeaderLink = React.memo(function HeaderLink(props: any) {
const HeaderLink = React.memo(function HeaderLink(props: {
title: string
id: string
url: string
}) {
const router = useRouter()
return (
@@ -3,13 +3,20 @@ import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FC, useEffect, useState } from 'react'
import { Button, IconCommand, IconMenu, IconMoon, IconSearch, IconSun, Listbox } from 'ui'
import {
Button,
IconCommand,
IconMenu,
IconMoon,
IconSearch,
IconSun,
Listbox,
SearchButton,
} from 'ui'
import { REFERENCES } from '~/components/Navigation/Navigation.constants'
import { getPageType } from '~/lib/helpers'
import SearchButton from './../../Search/SearchButton'
const TopNavBar: FC = () => {
const { isDarkMode, toggleTheme } = useTheme()
const [mounted, setMounted] = useState(false)
@@ -3,13 +3,11 @@ import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FC, useEffect, useState } from 'react'
import { Button, IconCommand, IconGitHub, IconMoon, IconSearch, IconSun } from 'ui'
import { Button, IconCommand, IconGitHub, IconMoon, IconSearch, IconSun, SearchButton } from 'ui'
import { REFERENCES } from '~/components/Navigation/Navigation.constants'
import { getPageType } from '~/lib/helpers'
import SearchButton from '~/components/Search/SearchButton'
const TopNavBarRef: FC = () => {
const { isDarkMode, toggleTheme } = useTheme()
const [mounted, setMounted] = useState(false)
@@ -128,16 +126,15 @@ const TopNavBarRef: FC = () => {
Dashboard
</Button>
<ul className="flex items-center">
<Button
type="text"
as="a"
// @ts-ignore
<Link
href="https://github.com/supabase/supabase"
target="_blank"
rel="noreferrer noopener"
>
<IconGitHub size={16} />
</Button>
<a className="px-2.5 py-1" target="_blank">
<IconGitHub size={16} />
</a>
</Link>
</ul>
<ul className="flex items-center">
<li className="px-4">
@@ -1,40 +0,0 @@
import {
ButtonHTMLAttributes,
DetailedHTMLProps,
FC,
PropsWithChildren,
useEffect,
useRef,
} from 'react'
import { useActionKey } from '~/hooks/useActionKey'
import { useSearch } from './SearchProvider'
const SearchButton: FC<
PropsWithChildren<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>>
> = ({ children, ...props }) => {
const searchButtonRef = useRef<HTMLButtonElement>()
const { open, setQuery } = useSearch()
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (searchButtonRef.current === document.activeElement) {
if (/[a-zA-Z0-9]/.test(String.fromCharCode(event.keyCode))) {
open()
setQuery?.(event.key)
}
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
}, [open, setQuery])
return (
<button type="button" ref={searchButtonRef} onClick={open} {...props}>
{children}
</button>
)
}
export default SearchButton
-423
View File
@@ -1,423 +0,0 @@
import type { CreateCompletionResponse } from 'openai'
import { FC, useCallback, useRef, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { SSE } from 'sse.js'
import clippyImageDark from '../../public/img/clippy-dark.png'
import clippyImage from '../../public/img/clippy.png'
import { useSupabaseClient } from '@supabase/auth-helpers-react'
import { useTheme } from 'common/Providers'
import Image from 'next/image'
import {
Button,
IconAlertCircle,
IconAlertTriangle,
IconLoader,
IconSearch,
Input,
Loading,
Modal,
Tabs,
} from 'ui'
import components from '~/components'
import { IS_PLATFORM } from '~/lib/constants'
import { useSearch } from './SearchProvider'
import SearchResult, { SearchResultType } from './SearchResult'
const questions = [
'How do I get started with Supabase?',
'How do I run Supabase locally?',
'How do I connect to my database?',
'How do I run migrations? ',
'How do I listen to changes in a table?',
'How do I set up authentication?',
]
function getEdgeFunctionUrl() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '')
if (IS_PLATFORM) {
const [schemeAndProjectId, domain, tld] = supabaseUrl.split('.')
return `${schemeAndProjectId}.functions.${domain}.${tld}`
} else {
return `${supabaseUrl}/functions/v1`
}
}
const edgeFunctionUrl = getEdgeFunctionUrl()
const SearchModal: FC = () => {
const { isDarkMode } = useTheme()
const { close, query, setQuery } = useSearch()
const [answer, setAnswer] = useState('')
const [results, setResults] = useState<any[]>()
const [isLoading, setIsLoading] = useState(false)
const [isResponding, setIsResponding] = useState(false)
const [hasClippyError, setHasClippyError] = useState(false)
const [hasSearchError, setHasSearchError] = useState(false)
const [selectedTab, setSelectedTab] = useState('search-panel')
const eventSourceRef = useRef<SSE>()
const supabaseClient = useSupabaseClient()
const cantHelp = answer?.trim() === "Sorry, I don't know how to help with that."
const status = isLoading
? 'Clippy is searching...'
: isResponding
? 'Clippy is responding...'
: cantHelp || hasClippyError
? 'Clippy has failed you'
: undefined
const handleSearchConfirm = useCallback(
async (query: string) => {
setResults(undefined)
setAnswer(undefined)
setIsResponding(false)
setHasClippyError(false)
setHasSearchError(false)
setIsLoading(true)
const { error, data: pageSections } = await supabaseClient.functions.invoke('search', {
body: { query },
})
setIsLoading(false)
if (error) {
setIsLoading(false)
setIsResponding(false)
setHasSearchError(true)
console.error(error)
return
}
if (!Array.isArray(pageSections)) {
setIsLoading(false)
setIsResponding(false)
setHasSearchError(true)
console.error('Malformed response')
return
}
setResults(pageSections)
},
[supabaseClient]
)
const handleClippyConfirm = useCallback(async (query: string) => {
setResults(undefined)
setAnswer(undefined)
setIsResponding(false)
setHasClippyError(false)
setHasSearchError(false)
setIsLoading(true)
const eventSource = new SSE(`${edgeFunctionUrl}/clippy-search`, {
headers: {
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`,
'Content-Type': 'application/json',
},
payload: JSON.stringify({ query }),
})
function handleError<T>(err: T) {
setIsLoading(false)
setIsResponding(false)
setHasClippyError(true)
console.error(err)
}
eventSource.addEventListener('error', handleError)
eventSource.addEventListener('message', (e) => {
try {
setIsLoading(false)
if (e.data === '[DONE]') {
setIsResponding(false)
return
}
setIsResponding(true)
const completionResponse: CreateCompletionResponse = JSON.parse(e.data)
const [{ text }] = completionResponse.choices
setAnswer((answer) => {
return (answer ?? '') + text
})
} catch (err) {
handleError(err)
}
})
eventSource.stream()
eventSourceRef.current = eventSource
setIsLoading(true)
}, [])
const handleConfirm = useCallback(
(selectedTab: string, query: string) => {
switch (selectedTab) {
case 'search-panel':
return handleSearchConfirm(query)
case 'clippy-panel':
return handleClippyConfirm(query)
}
},
[handleSearchConfirm, handleClippyConfirm]
)
function handleResetPrompt() {
eventSourceRef.current?.close()
eventSourceRef.current = undefined
setQuery('')
setResults(undefined)
setAnswer(undefined)
setIsResponding(false)
setHasClippyError(false)
setHasSearchError(false)
}
return (
<Modal size="xlarge" visible={true} onCancel={close} closable={false} hideFooter>
<div
className={`mx-auto max-h-[90vh] lg:max-h-[75vh] flex flex-col gap-4 rounded-lg p-4 md:pt-6 md:px-6 pb-2 w-full shadow-2xl overflow-hidden border text-left border-scale-500 bg-scale-100 dark:bg-scale-300 cursor-auto relative min-w-[340px]`}
onClick={(e) => e.stopPropagation()}
>
<div className="relative">
<Input
className="w-full"
size="xlarge"
autoFocus
placeholder={selectedTab === 'search-panel' ? 'Search documentation' : 'Ask a question'}
value={query}
onChange={(e) => setQuery(e.target.value)}
icon={<IconSearch size="small" />}
onKeyDown={(e) => {
switch (e.key) {
case 'Enter':
if (!query) {
return
}
handleConfirm(selectedTab, query)
return
default:
return
}
}}
/>
<div className="absolute right-0 top-0 mt-3 mr-4 hidden md:block">
<Button type="default" size="tiny" onClick={close}>
esc
</Button>
</div>
{!isLoading && answer && (
<div className="absolute right-0 top-0 mt-3 mr-16 hidden md:block">
<Button type="text" size="tiny" onClick={handleResetPrompt}>
Try again
</Button>
</div>
)}
</div>
<Tabs
activeId={selectedTab}
onChange={(tabId) => {
setSelectedTab(tabId)
if (!query) {
handleResetPrompt()
return
}
handleConfirm(tabId, query)
}}
>
<Tabs.Panel id="search-panel" label="Guides & Reference">
<div className="mb-6">
{!isLoading && !hasSearchError && !results && (
<div className="p-10 grid">
<h2 className="text-lg text-center text-scale-1100">
Search Supabase guides & reference docs
</h2>
</div>
)}
{results && results.length > 0 && (
<div className="flex flex-col gap-3 max-h-[70vh] lg:max-h-[50vh] overflow-y-auto px-4 py-4 rounded-lg bg-scale-200">
{results.map((page) => {
const pageSections = page.sections.filter((section) => !!section.heading)
return (
<div key={page.id} className="flex flex-col gap-3">
<SearchResult
href={page.path}
type={SearchResultType.Document}
title={page.meta.title}
/>
{pageSections.length > 0 && (
<div className="flex flex-row">
<div className="border bg-scale-300 rounded-xl self-stretch p-[1px] ml-4 mr-4"></div>
<div className="flex flex-col gap-3 items-stretch grow">
{pageSections.map((section) => (
<SearchResult
key={section.id}
href={`${page.path}${page.type === 'reference' ? '/' : '#'}${
section.slug
}`}
type={SearchResultType.Section}
title={section.heading}
chip={page.meta.title}
/>
))}
</div>
</div>
)}
</div>
)
})}
</div>
)}
{isLoading && (
<div className="p-6 grid gap-6 mt-4">
<Loading active>{}</Loading>
<p className="text-lg text-center">Searching for results</p>
</div>
)}
{results && results.length === 0 && (
<div className="p-6 flex flex-col items-center gap-6 mt-4">
<IconAlertTriangle strokeWidth={1.5} size={40} />
<p className="text-lg text-center">No results found.</p>
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
Try again?
</Button>
</div>
)}
{hasSearchError && (
<div className="p-6 flex flex-col items-center gap-6 mt-4">
<IconAlertTriangle strokeWidth={1.5} size={40} />
<p className="text-lg text-center">
Sorry, looks like we&apos;re having some issues with search!
</p>
<p className="text-sm text-center">Please try again in a bit.</p>
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
Try again?
</Button>
</div>
)}
</div>
</Tabs.Panel>
<Tabs.Panel id="clippy-panel" label="Ask Clippy">
{!isLoading && !answer && !hasClippyError && (
<div className="">
<div className="mt-2">
<h2 className="text-sm text-scale-1100">Not sure where to start?</h2>
<ul className="text-sm mt-4 text-scale-1100 grid md:flex gap-4 flex-wrap max-w-3xl">
{questions.map((question) => {
const key = question.replace(/\s+/g, '_')
return (
<li key={key}>
<button
className="hover:bg-slate-400 hover:dark:bg-slate-400 px-4 py-2 bg-slate-300 dark:bg-slate-200 rounded-lg transition-colors"
onClick={() => {
setQuery(question)
handleClippyConfirm(question)
}}
>
{question}
</button>
</li>
)
})}
</ul>
</div>
</div>
)}
{answer && (
<div className="px-4 py-4 rounded-lg overflow-y-auto bg-scale-200 max-h-[70vh] lg:max-h-[50vh]">
{cantHelp ? (
<p className="flex flex-col gap-4 items-center p-4">
<div className="grid md:flex items-center gap-2 mt-4 text-center justify-items-center">
<IconAlertCircle />
<p>Sorry, I don&apos;t know how to help with that.</p>
</div>
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
Try again?
</Button>
</p>
) : (
<div className="prose dark:prose-dark">
<ReactMarkdown
linkTarget="_blank"
remarkPlugins={[remarkGfm]}
transformLinkUri={(href) => {
const supabaseUrl = new URL('https://supabase.com')
const linkUrl = new URL(href, 'https://supabase.com')
if (linkUrl.origin === supabaseUrl.origin) {
return linkUrl.toString()
}
return href
}}
components={components}
>
{answer}
</ReactMarkdown>
</div>
)}
</div>
)}
{isLoading && (
<div className="p-6 grid gap-6 mt-4">
<Loading active>{}</Loading>
<p className="text-lg text-center">Searching for results</p>
</div>
)}
{hasClippyError && (
<div className="p-6 flex flex-col items-center gap-6 mt-4">
<IconAlertTriangle strokeWidth={1.5} size={40} />
<p className="text-lg text-center">
Sorry, looks like Clippy is having a hard time!
</p>
<p className="text-sm text-center">Please try again in a bit.</p>
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
Try again?
</Button>
</div>
)}
<div className="border-t border-scale-600 mt-4 text-scale-1100">
<div className="flex justify-between items-center py-2 text-xs">
<div className="flex items-centerp gap-1 pt-3 pb-1">
<span>Powered by OpenAI.</span>
<a href="/blog/chatgpt-supabase-docs" className="underline">
Read the blog post
</a>
</div>
<div className="flex items-center gap-6 py-1">
{status ? (
<span className="bg-scale-400 rounded-lg py-1 px-2 items-center gap-2 hidden md:flex">
{(isLoading || isResponding) && (
<IconLoader size={14} className="animate-spin" />
)}
{status}
</span>
) : (
<></>
)}
<Image
width={30}
height={34}
src={isDarkMode ? clippyImageDark : clippyImage}
alt="Clippy"
/>
</div>
</div>
</div>
</Tabs.Panel>
</Tabs>
</div>
</Modal>
)
}
export default SearchModal
@@ -1,80 +0,0 @@
import {
createContext,
FC,
PropsWithChildren,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import { createPortal } from 'react-dom'
import SearchModal from './SearchModal'
export type SearchContextValue = {
isOpen: boolean
open: () => void
close: () => void
query: string
setQuery: (query: string) => void
}
export const SearchContext = createContext<SearchContextValue>(null)
export const useSearch = () => {
const { isOpen, open, close, query, setQuery } = useContext(SearchContext)
return { isOpen, open, close, query, setQuery }
}
const SearchProvider: FC<PropsWithChildren<{}>> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false)
const [query, setQuery] = useState('')
const open = useCallback(() => {
setIsOpen(true)
document.body.classList.add('DocSearch--active')
}, [])
const close = useCallback(() => {
setIsOpen(false)
document.body.classList.remove('DocSearch--active')
}, [])
useSearchKeyboardEvents({
open,
close,
})
return (
<SearchContext.Provider value={{ isOpen, open, close, query, setQuery }}>
{children}
{isOpen && createPortal(<SearchModal />, document.body)}
</SearchContext.Provider>
)
}
function useSearchKeyboardEvents({ open, close }) {
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 'Escape':
close()
return
case 'k':
case '/':
if (event.metaKey || event.ctrlKey) {
open()
}
return
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
}, [open, close])
}
export default SearchProvider
@@ -1,54 +0,0 @@
import Link from 'next/link'
import { FC } from 'react'
import { IconBookOpen, IconHash } from '~/../../packages/ui'
import { useSearch } from './SearchProvider'
export enum SearchResultType {
Document = 'document',
Section = 'section',
}
type Props = {
href: string
type: SearchResultType
title: string
chip?: string
}
const SearchResult: FC<Props> = ({ href, type, title, chip }) => {
const { close } = useSearch()
return (
<Link href={href}>
<a
className="flex flex-row items-center bg-scale-400 hover:bg-scale-600 transition p-4 rounded-md border border-scale-600 text-sm cursor-pointer"
onClick={close}
>
<div className="w-6 h-6 p-1 flex items-center justify-center mr-4 text-brand-1100 rounded-md bg-scale-700">
{getIconByType(type)}
</div>
<div className="flex flex-col gap-2 items-start">
{chip && (
<div className="rounded-xl bg-scale-700 pl-3 pr-3 pt-0.5 pb-0.5 text-xs text-scale-1100">
{chip}
</div>
)}
<div>{title}</div>
</div>
</a>
</Link>
)
}
function getIconByType(type: SearchResultType) {
switch (type) {
case SearchResultType.Document:
return <IconBookOpen />
case SearchResultType.Section:
return <IconHash />
default:
throw new Error(`Unknown search result type '${type}'`)
}
}
export default SearchResult
+4
View File
@@ -22,6 +22,10 @@ const levelsData = {
icon: '/docs/img/icons/menu/database',
name: 'Database',
},
api: {
icon: '/docs/img/icons/menu/database',
name: 'Serverless APIs',
},
auth: {
icon: '/docs/img/icons/menu/auth',
name: 'Auth',
+1
View File
@@ -65,6 +65,7 @@
"mdast-util-to-markdown": "^1.5.0",
"mdast-util-to-string": "^3.1.1",
"mdx-mermaid": "2.0.0-rc3",
"mermaid": "^10.0.2",
"micromark-extension-mdxjs": "^1.0.0",
"next": "12.3.2",
"next-compose-plugins": "^2.2.1",
+28 -31
View File
@@ -3,16 +3,16 @@ import { SessionContextProvider } from '@supabase/auth-helpers-react'
import { AuthProvider, ThemeProvider } from 'common'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { AppPropsWithLayout } from 'types'
// import { CommandMenuProvider } from 'ui'
import { CommandMenuProvider } from 'ui'
import components from '~/components'
import Favicons from '~/components/Favicons'
import SearchProvider from '~/components/Search/SearchProvider'
import SiteLayout from '~/layouts/SiteLayout'
import { IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants'
import { post } from '~/lib/fetchWrappers'
import '../styles/algolia-search.scss'
import '../styles/ch.scss'
import '../styles/docsearch.scss'
import '../styles/main.scss?v=1.0.0'
import '../styles/new-docs.scss'
import '../styles/prism-okaidia.scss'
@@ -61,36 +61,33 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const SITE_TITLE = 'Supabase Documentation'
const AuthContainer = (props) => {
return IS_PLATFORM || LOCAL_SUPABASE ? (
<SessionContextProvider supabaseClient={supabase}>
<AuthProvider>{props.children}</AuthProvider>
</SessionContextProvider>
) : (
<AuthProvider>{props.children}</AuthProvider>
)
}
return (
<>
<Favicons />
{IS_PLATFORM || LOCAL_SUPABASE ? (
<SessionContextProvider supabaseClient={supabase}>
<AuthProvider>
<ThemeProvider>
<SearchProvider>
{/* <CommandMenuProvider site="docs"> */}
<SiteLayout>
<Component {...pageProps} />
</SiteLayout>
{/* </CommandMenuProvider> */}
</SearchProvider>
</ThemeProvider>
</AuthProvider>
</SessionContextProvider>
) : (
<AuthProvider>
<ThemeProvider>
<SearchProvider>
{/* <CommandMenuProvider site="docs"> */}
<SiteLayout>
<Component {...pageProps} />
</SiteLayout>
{/* </CommandMenuProvider> */}
</SearchProvider>
</ThemeProvider>
</AuthProvider>
)}
<AuthContainer>
<ThemeProvider>
<CommandMenuProvider
site="docs"
MarkdownHandler={({ ...props }) => (
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components} {...props} />
)}
>
<SiteLayout>
<Component {...pageProps} />
</SiteLayout>
</CommandMenuProvider>
</ThemeProvider>
</AuthContainer>
</>
)
}
+52
View File
@@ -0,0 +1,52 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'api',
title: 'Serverless APIs',
description: 'Auto-generating and Realtime APIs.',
sidebar_label: 'Overview',
video: 'https://www.youtube.com/v/rPAJJFdtPw0',
}
Supabase auto-generates three types of API directly from your database schema.
- REST - connect to your database through a restful interface, directly from the browser.
- Realtime - for listening to database changes.
- GraphQL - a fast and auto-generate GraphQL API.
All the APIs are auto-generated from your database and are designed to get you building as fast as possible, without writing a single line of code.
You can use them directly from the browser (two-tier architecture), or as a complement to your own API server (three-tier architecture).
## Features
- **Instant and auto-generated.** <br />As you update your database the changes are immediately accessible through your API.
- **Self documenting.** <br />Supabase generates documentation in the Dashboard which updates as you make database changes.
- **Secure.** <br />The API is configured to work with PostgreSQL's Row Level Security, provisioned behind an API gateway with key-auth enabled.
- **Fast.** <br />Our benchmarks for basic reads are more than 300% faster than Firebase. The API is a very thin layer on top of Postgres, which does most of the heavy lifting.
- **Scalable.** <br />The API can serve thousands of simultaneous requests, and works well for Serverless workloads.
## REST API [#rest-api-overview]
Supabase provides a RESTful API using [PostgREST](https://postgrest.org/). This is a very thin API layer on top of Postgres.
It provides everything you need from a CRUD API:
- Basic CRUD operations
- Deeply nested joins, allowing you to fetch data from multiple tables in a single fetch
- Works with Postgres Views
- Works with Postgres Functions
- Works with the Postgres security model - including Row Level Security, Roles, and Grants.
## GraphQL API [#graphql-api-overview]
GraphQL in Supabase works through [pg_graphql](https://supabase.com/blog/pg-graphql), an open source PostgreSQL extension for GraphQL.
## Realtime API [#realtime-api-overview]
Supabase provides a Realtime API using [Realtime](https://github.com/supabase/realtime). You can use this to listen to database changes over websockets.
Realtime leverages PostgreSQL's built-in logical replication. You can manage your Realtime API simply by managing Postgres publications.
Go to your project's [Replication section](https://app.supabase.com/project/_/database/replication) to get started.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
+70
View File
@@ -0,0 +1,70 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'api-keys',
title: 'Understanding API Keys',
description: 'Securing your Serverless API with Postgres Row Level Security.',
}
Supabase provides two default keys when you create a project: an `anon` key, and a `service_role` key. You can find both keys in the [API Settings](https://app.supabase.com/project/_/settings/api).
The Serverless APIs are designed to work with Postgres Row Level Security (RLS). These keys both map to Postgres roles. You can find an `anon` user and a `service_role` user in the [Roles](http://app.supabase.com/project/_/database/roles) section of the dashboard.
The keys are both long-lived JWTs. If you decode these keys, you will see that they contain the "role", an "issued date", and an "expiry date" ~10 years in the future.
```json
{
"role": "anon",
"iat": 1625137684,
"exp": 1940713684
}
```
## The `anon` key
The `anon` key has very few privileges. You can use it in your [RLS policies](/docs/guides/auth/row-level-security) for "anonymous" access. For example, this policy will allow access to the `profiles` table:
```sql
create policy "Allow anonymous access" on profiles to anon for
select
using (true);
```
And similarity for disallowing access:
```sql
create policy "Disallow anonymous access" on profiles to anon for
select
using (false);
```
If you are using [Supabase Auth](/docs/guides/auth/overview), then the `anon` role will automatically update to `authenticated` once a user is logged in:
```sql
create policy "Allow access to authenticated users" on profiles to authenticated for
select
using (true);
```
## The `service_role` key
The "service_role" is a predefined Postgres role with elevated privileges, designed to perform various administrative and service-related tasks. It can bypass Row Level Security, so it should only be used on a private server.
<Admonition type="caution">
Never expose the `service_role` key in a browser or anywhere where a user can see it.
</Admonition>
A common use case for the `service_role` key is running data analytics jobs on the backend. To support joins on user id, it is often useful to grant the service role read access to `auth.users` table.
```sql
grant
select
on table auth.users to service_role;
```
We have [partnered with GitHub](https://github.blog/changelog/2022-03-28-supabase-is-now-a-github-secret-scanning-partner/) to scan for Supabase `service_role` keys pushed to public repositories.
If they detect any keys with service_role privileges being pushed to GitHub, they will forward the API key to us, so that we can automatically revoke the detected secrets and notify you, protecting your data against malicious actors.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,252 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'creating-routes',
title: 'Creating API Routes',
description:
'API routes are automatically created when you create Postgres Tables, Views, or Functions.',
}
API routes are automatically created when you create Postgres Tables, Views, or Functions.
## Create a table
Let's create our first API route by creating a table called `todos` to store tasks.
This creates a corresponding route `todos` which can accept `GET`, `POST`, `PATCH`, & `DELETE` requests.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Table editor](https://app.supabase.com/project/_/editor) page in the Dashboard.
1. Click **New Table** and create a table with the name `todos`.
1. Click **Save**.
1. Click **New Column** and create a column with the name `task` and type `text`.
1. Click **Save**.
<video width="99%" muted playsInline controls="true">
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
-- Create a table called "todos" with a column to store tasks.
create table
todos (
id bigint generated by default as identity primary key,
task text check (char_length(task) > 3)
);
```
</TabPanel>
</Tabs>
## API URL and Keys
Every Supabase project has a unique API URL. Your API is secured behind an API gateway which requires an API Key for every request.
1. Go to the [Settings](https://app.supabase.com/project/_/settings/general) page in the Dashboard.
2. Click **API** in the sidebar.
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
<video width="99%" muted playsInline controls="true">
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
The REST API and the GraphQL API are both accessible through this URL:
- REST: `https://<project_ref>.supabase.co/rest/v1`
- GraphQL: `https://<project_ref>.supabase.co/graphql/v1`
Both of these routes require the `anon` key to be passed through an `apikey` header.
## Using the API
### REST API
You can interact with your API directly via HTTP requests, or you can use the client libraries which we provide.
Let's see how to make a request to the `todos` table which we created in the first step,
using the API URL (`SUPABASE_URL`) and Key (`SUPABASE_ANON_KEY`) we provided:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="javascript"
>
<TabPanel id="javascript" label="Javascript">
```javascript
// Initialize the JS client
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
// Make a request
const { data: todos, error } = await supabase.from('todos').select('*')
```
</TabPanel>
<TabPanel id="curl" label="cURL">
```bash
# Append /rest/v1/ to your URL, and then use the table name as the route
curl '<SUPABASE_URL>/rest/v1/todos' \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <SUPABASE_ANON_KEY>"
```
</TabPanel>
</Tabs>
JS Reference: [select()](/docs/reference/javascript/select),
[insert()](/docs/reference/javascript/insert),
[update()](/docs/reference/javascript/update),
[upsert()](/docs/reference/javascript/upsert),
[delete()](/docs/reference/javascript/delete),
[rpc()](/docs/reference/javascript/rpc) (call Postgres functions).
### GraphQL API
You can use any GraphQL client with the Supabase GraphQL API. For our GraphQL example we will use [urql](https://formidable.com/open-source/urql/docs/).
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="javascript"
>
<TabPanel id="javascript" label="Javascript">
```javascript
import { createClient, useQuery } from 'urql'
// Prepare API key and Authorization header
const headers = {
apikey: <SUPABASE_ANON_KEY>,
authorization: `Bearer ${<SUPABASE_ANON_KEY>}`,
}
// Create GraphQL client
// See: https://formidable.com/open-source/urql/docs/basics/react-preact/#setting-up-the-client
const client = createClient({
url: '<SUPABASE_URL>/graphql/v1',
fetchOptions: function createFetchOptions() {
return { headers }
},
})
// Prepare our GraphQL query
const TodosQuery = `
query {
todosCollection {
edges {
node {
id
title
}
}
}
}
`
// Query for the data (React)
const [result, reexecuteQuery] = useQuery({
query: TodosQuery,
})
// Read the result
const { data, fetching, error } = result
```
</TabPanel>
<TabPanel id="curl" label="cURL">
```bash
# Append /graphql/v1/ to your URL, and then use the table name as the route
curl --request POST '<SUPABASE_URL>/graphql/v1' \
-H 'apikey: <SUPABASE_ANON_KEY>' \
-H 'Authorization: Bearer <SUPABASE_ANON_KEY>' \
-H 'Content-Type: application/json' \
-d '{ "query":"{ todos(first: 3) { edges { node { id } } } }" }'
```
</TabPanel>
</Tabs>
### Realtime API
By default Realtime is disabled on your database. Let's turn on Realtime for the `todos` table.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Replication** in the sidebar.
3. Control which database events are sent by toggling **Insert**, **Update**, and **Delete**.
4. Control which tables broadcast changes by selecting **Source** and toggling each table.
<video width="99%" muted playsInline controls="true">
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-realtime.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
alter
publication supabase_realtime add table todos;
```
</TabPanel>
</Tabs>
From the client, we can listen to any new data that is inserted into the `todos` table:
```javascript
// Initialize the JS client
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
// Create a function to handle inserts
const handleInserts = (payload) => {
console.log('Change received!', payload)
}
// Listen to inserts
const { data: todos, error } = await supabase.from('todos').on('INSERT', handleInserts).subscribe()
```
Use [subscribe()](/docs/reference/javascript/subscribe) to listen to database changes.
The Realtime API works through PostgreSQL's replication functionality. Postgres sends database changes to a [publication](/docs/guides/database/replication#publications)
called `supabase_realtime`, and by managing this publication you can control which data is broadcast.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,22 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'graphiql',
title: 'GraphiQL documentation',
description: 'Some docs on graphiql.',
video: 'https://www.youtube.com/v/7CqlTU9aOR4',
}
Every Supabase project has a GraphQL Endpoint: `https://<project_ref>.supabase.co/graphql/v1`.
This endpoint is compatible with any GraphiQL implementation that can pass an `apikey` header.
Some suggested applications:
- [paw.cloud](https://paw.cloud)
- [insomnia.rest](https://insomnia.rest)
- [postman.com/graphql](https://www.postman.com/graphql/)
- Self-hosted GraphiQL: GraphiQL can be served through a simple HTML file. See [this discussion](https://github.com/supabase/supabase/discussions/6144) for more details.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,206 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'joins-and-nested-tables',
title: 'Querying Joins and Nested tables',
description: 'The Serverless APIs automatically detect relationships between Postgres tables.',
}
The Serverless APIs automatically detect relationships between Postgres tables. Since Postgres is a relational database, this is a very common scenario.
## One-to-many joins
Let's use an example database that stores `countries` and `cities`:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="table"
>
<TabPanel id="table" label="Tables">
**Countries**
| `id` | `name` |
| ---- | -------------- |
| 1 | United Kingdom |
| 2 | United States |
**Cities**
| `id` | `name` | `country_id` |
| ---- | ----------- | ------------ |
| 1 | London | 1 |
| 2 | Manchester | 1 |
| 3 | Los Angeles | 2 |
| 4 | New York | 2 |
</TabPanel>
<TabPanel id="SQL" label="SQL">
```sql
create table
countries ("id" serial primary key, "name" text);
insert into
countries (id, name)
values
(1, 'United Kingdom'),
(2, 'United States');
create table
cities (
"id" serial primary key,
"name" text,
"country_id" int references "countries"
);
insert into
cities (name, country_id)
values
('London', 1),
('Manchester', 1),
('Los Angeles', 2),
('New York', 2);
```
</TabPanel>
</Tabs>
The APIs will automatically detect relationships based on the foreign keys:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.from('countries').select(`
id,
name,
cities ( id, name )
`)
```
</TabPanel>
<TabPanel id="dart" label="Dart">
```dart
final data = await supabase.from('todos').select('id, name, cities(id, name)');
```
</TabPanel>
<TabPanel id="graphql" label="GraphQL">
```javascript
const Query = `
query {
countriesCollection {
edges {
node {
id
name
cities {
id,
name
}
}
}
}
}
`
```
</TabPanel>
<TabPanel id="url" label="URL">
```bash
GET https://[REF].supabase.co/rest/v1/countries?select=id,name,cities(id,name)
```
</TabPanel>
</Tabs>
## Many-to-many joins
The Serverless APIs will detect many-to-many joins. For example, if you have a database which stored teams of users (where each user could below to many teams):
```sql
create table
users ("id" serial primary key, "name" text);
create table
teams ("id" serial primary key, "team_name" text);
create table
members (
"id" serial primary key,
"user_id" int references users,
"team_id" int references teams
);
```
In these cases you don't need to explicitly define the joining table (members). If we wanted to fetch all the teams and the members in each team:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.from('teams').select(`
id,
team_name,
users ( id, name )
`)
```
</TabPanel>
<TabPanel id="dart" label="Dart">
```dart
final data = await supabase.from('teams').select('id, team_name, users(id, name)');
```
</TabPanel>
<TabPanel id="graphql" label="GraphQL">
```javascript
const Query = `
query {
teamsCollection {
edges {
node {
id
team_name
users {
id,
name
}
}
}
}
}
`
```
</TabPanel>
<TabPanel id="url" label="URL">
```bash
GET https://[REF].supabase.co/rest/v1/teams?select=id,team_name,users(id,name)
```
</TabPanel>
</Tabs>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
+236
View File
@@ -0,0 +1,236 @@
import Layout from '~/layouts/DefaultGuideLayout'
import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
title: 'Build an API route in less than 2 minutes.',
subtitle: 'Create your first API route by creating a table called `todos` to store tasks.',
breadcrumb: 'API Quickstart',
}
Let's create our first REST route which we can query using `cURL` or the browser.
We'll create a database table called `todos` for storing tasks. This creates a corresponding API route `/rest/v1/todos` which can accept `GET`, `POST`, `PATCH`, & `DELETE` requests.
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Set up a Supabase project with a 'todos' table">
[Create a new project](https://app.supabase.com) in the Supabase Dashboard.
After your project is ready, create a table in your Supabase database. You can do this with either the Table interface or the [SQL Editor](https://app.supabase.com/project/_/sql).
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="sql"
>
<TabPanel id="sql" label="SQL">
```sql
-- Create a table called "todos"
-- with a column to store tasks.
create table todos (
id serial primary key,
task text
);
```
</TabPanel>
<TabPanel id="dashboard" label="Dashboard">
<video width="99%" muted playsInline controls="true">
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
</TabPanel>
</Tabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Allow anonymous access">
Let's turn on Row Level Security for this table and allow anonymous access.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sql
-- Turn on security
alter table "todos"
enable row level security;
-- Allow anonymous access
create policy "Allow anonymous access"
on todos
to anon
for select
using (true);
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Insert some dummy data">
Now we can add some data to our table which we can access through our API.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sql
insert into todos (task)
values
('Create tables'),
('Enable security'),
('Add data'),
('Fetch data from the API');
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Fetch the data">
Find your API URL and Keys in your Dashboard [API Settings](https://app.supabase.com/project/_/settings/api). You can now query your "todos" table by appending `/rest/v1/todos` to the API URL.
Copy this block of code, substitute `<PROJECT_REF>` and `<ANON_KEY>`, then run it from a terminal.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash Terminal
curl 'https://<PROJECT_REF>.supabase.co/rest/v1/todos' \
-H "apikey: <ANON_KEY>" \
-H "Authorization: Bearer <ANON_KEY>"
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
## Bonus
There are several options for accessing your data:
### Browser
You can query the route in your browser, by appending the `anon` key as a query parameter:
`https://<PROJECT_REF>.supabase.co/rest/v1/users?apikey=<ANON_KEY>`
### Client libraries
We provide a numerous [Client Libraries](https://github.com/supabase/supabase#client-libraries).
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.from('todos').select()
```
</TabPanel>
<TabPanel id="dart" label="Dart">
```dart
final data = await supabase.from('todos').select('*');
```
</TabPanel>
<TabPanel id="python" label="Python">
```python
response = supabase.table('todos').select("*").execute()
```
</TabPanel>
</Tabs>
### GraphQL
Every table can be accessed through the GraphQL API by switching `/rest/v1` with `/graphql/v1`.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="urql"
>
<TabPanel id="urql" label="urql">
```javascript
import { createClient, useQuery } from 'urql'
const URL = '<SUPABASE_URL>/graphql/v1'
const ANON_KEY = '<SUPABASE_ANON_KEY>'
// Prepare API key and Authorization header
const headers = {
apikey: `${ANON_KEY}`,
authorization: `Bearer ${ANON_KEY}`,
}
const client = createClient({
url: URL,
fetchOptions: function createFetchOptions() {
return { headers }
},
})
// Prepare our GraphQL query
const TodosQuery = `
query {
todosCollection {
edges {
node {
id
task
}
}
}
}
`
// Query for the data (React)
const [result, reexecuteQuery] = useQuery({
query: TodosQuery,
})
// Read the result
const { data, fetching, error } = result
```
</TabPanel>
</Tabs>
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
export default Page
@@ -0,0 +1,26 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auto-docs',
title: 'Auto-generated documentation',
description: 'Supabase provides documentation that updates automatically.',
}
Supabase generates documentation in the [Dashboard](https://app.supabase.com) which updates as you make database changes.
1. Go to the [API](https://app.supabase.com/project/_/api) page in the Dashboard.
2. Select any table under **Tables and Views** in the sidebar.
3. Switch between the JavaScript and the cURL docs using the tabs.
<video width="99%" muted playsInline controls="true">
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-docs.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,33 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'client-libs',
title: 'Client Libraries',
description: 'Supabase provides several client libraries for the REST and Realtime APIs.',
video: 'https://www.youtube.com/v/7CqlTU9aOR4',
}
Supabase provides client libraries for the REST and Realtime APIs. Some libraries are officially supported, and some are contributed by the community.
## Official Libraries
| `Language` | `Source Code` | `Documentation` |
| --------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- |
| Javascript/Typescript | [supabase-js](https://github.com/supabase/supabase-js) | [Docs](https://supabase.com/docs/reference/javascript/introduction) |
| Dart/Flutter | [supabase-dart](https://github.com/supabase/supabase-dart) | [Docs](https://supabase.com/docs/reference/dart/introduction) |
## Community Libraries
| `Language` | `Source Code` | `Documentation` |
| ----------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| C# | [supabase-js](https://github.com/supabase-community/supabase-csharp) | [Docs](https://supabase.com/docs/reference/csharp/introduction) |
| Go | [supabase-go](https://github.com/supabase-community/supabase-go) | |
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | |
| Python | [supabase-py](https://github.com/supabase-community/supabase-py) | [Docs](https://supabase.com/docs/reference/python/initializing) |
| Ruby | [supabase-rb](https://github.com/supabase-community/supabase-rb) | |
| Swift | [supabase-swift](https://github.com/supabase-community/supabase-swift) | |
| Godot Engine (GDScript) | [supabase-gdscript](https://github.com/supabase-community/godot-engine.supabase) | |
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,66 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'securing-your-api',
title: 'Securing your API',
description: 'Securing your Serverless API with Postgres Row Level Security.',
}
The Serverless APIs are designed to work with Postgres Row Level Security (RLS). If you use [Supabase Auth](/docs/guides/auth), you can restrict data based on the logged-in user.
To control access to your data, you can use [Policies](/docs/guides/auth#policies).
## Enabling Row Level Security
If you create a table through the Dashboard, RLS will be enabled by default. This is not the case, however, if you create a table or view using SQL.
To enable RLS on any table:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Authentication](https://app.supabase.com/project/_/auth/users) page in the Dashboard.
2. Click on **Policies** in the sidebar.
3. Select **Enable RLS** to enable Row Level Security.
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
alter table
todos enable row level security;
```
</TabPanel>
</Tabs>
With RLS enabled, you can create Policies that allow or disallow users to access and update data. We provide a detailed guide for creating Row Level Security Policies in our [Authorization documentation](/docs/guides/auth/row-level-security).
## Safeguards towards accidental deletes and updates
By default, all projects have the [safeupdate](https://github.com/eradman/pg-safeupdate) Postgres extension enabled for API queries.
This ensures that `delete()` and `update()` requests will fail if there are no filters provided.
To confirm that safeupdate is enabled for API queries, run the following query:
```sql
select
usename,
useconfig
from
pg_shadow
where
usename = 'authenticator';
```
The expected value for `useconfig` should be:
```sql
['session_preload_libraries=supautils, safeupdate']
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -2,13 +2,15 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'nextjs-server-components',
title: 'Supabase Auth with Next.js Server Components',
title: 'Supabase Auth with Next.js app directory',
description:
'Authentication helpers for creating an authenticated Supabase client in Next.js 13 app directory Server Components.',
sidebar_label: 'Next.js Server Components',
'Authentication helpers for creating an authenticated Supabase client in Next.js 13 app directory Server Components and Route Handlers.',
sidebar_label: 'Next.js (app)',
}
This submodule provides experimental convenience helpers for implementing user authentication in Next.js Server Components - the `app` directory. For examples using the `pages` directory check out [Auth Helpers in Next.js](/docs/guides/auth/auth-helpers/nextjs).
The Next.js Auth Helpers package configures Supabase Auth to store the user's session in a cookie, rather than `localStorage`. This makes the users's session available server-side - in Server Components and Route Handlers - and is automatically sent along with any requests to Supabase.
> The `app` directory in Next.js is still in beta and expected to change. For examples using the `pages` directory check out [Auth Helpers in Next.js](/docs/guides/auth/auth-helpers/nextjs).
<div className="video-container">
<iframe
@@ -19,9 +21,7 @@ This submodule provides experimental convenience helpers for implementing user a
></iframe>
</div>
> For a complete implementation example, check out [this repo](https://github.com/supabase/auth-helpers/tree/main/examples/nextjs-server-components).
> To learn more about fetching and caching Supabase data with Next.js 13 Server Components, check out our [blog](/blog/fetching-and-caching-supabase-data-in-next-js-server-components) or [live stream](https://www.youtube.com/watch?v=QH0P5xZt5wY).
> To learn more about Supabase and the Next.js 13 app directory, check out [this playlist](https://youtube.com/playlist?list=PL5S4mPUpp4OtwG-qCxm8gA_hjaBq0OPdz).
## Install the Next.js helper library
@@ -61,9 +61,7 @@ NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
```
## Creating a Supabase Client
### Server-side
## Configure Middleware
<Tabs
scrollable
@@ -73,62 +71,9 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
>
<TabPanel id="js" label="JavaScript">
Create a new file at `/utils/supabase-server.js` and populate with the following:
Middleware runs immediately before each route in rendered. Next.js only provides read access to headers and cookies in Server Components and Route Handlers, however, Supabase needs to be able to set cookies and headers to refresh expired access tokens. Therefore, you must call the `getSession` function in `middleware.js` in order to use a Supabase client in Server Components or Route Handlers.
```js title="/utils/supabase-server.js"
import { headers, cookies } from 'next/headers'
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
export const createClient = () =>
createServerComponentSupabaseClient({
headers,
cookies,
})
```
> This needs to export a function, as the headers and cookies are not populated with values until the Server Component is requesting data.
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Create a new file at `/utils/supabase-server.ts` and populate with the following:
```ts title="/utils/supabase-server.ts"
import { headers, cookies } from 'next/headers'
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import type { Database } from '../lib/database.types'
export const createClient = () =>
createServerComponentSupabaseClient<Database>({
headers,
cookies,
})
```
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createServerSupabaseClient` to add type support to the Supabase client.
> This needs to export a function, as the headers and cookies are not populated with values until the Server Component is requesting data.
</TabPanel>
</Tabs>
This will be used any time we need to create a Supabase client _server-side_ - in a Server Component, for example.
Next, we need a middleware file to refresh the user's session on navigation.
> If you were using Middleware prior to 12.2, see the [upgrade guide](https://nextjs.org/docs/messages/middleware-upgrade-guide).
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Create a new `middleware.js` file at the same level as your `app` (in the root or `src` directory) and populate with the following:
Create a new `middleware.js` file in the root of your project and populate with the following:
```jsx title="middleware.js"
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
@@ -136,13 +81,8 @@ import { NextResponse } from 'next/server'
export async function middleware(req) {
const res = NextResponse.next()
const supabase = createMiddlewareSupabaseClient({ req, res })
const {
data: { session },
} = await supabase.auth.getSession()
await supabase.auth.getSession()
return res
}
```
@@ -151,24 +91,21 @@ export async function middleware(req) {
<TabPanel id="ts" label="TypeScript">
Create a new `middleware.ts` file at the same level as your `app` (in the root or `src` directory) and populate with the following:
Middleware runs immediately before each route in rendered. Next.js only provides read access to headers and cookies in Server Components and Route Handlers, however, Supabase needs to be able to set cookies and headers to refresh expired access tokens. Therefore, you must call the `getSession` function in `middleware.ts` in order to use a Supabase client in Server Components or Route Handlers.
Create a new `middleware.ts` file in the root of your project and populate with the following:
```tsx title="middleware.ts"
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import type { Database } from './lib/database.types'
import type { Database } from '@/lib/database.types'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareSupabaseClient<Database>({ req, res })
const {
data: { session },
} = await supabase.auth.getSession()
await supabase.auth.getSession()
return res
}
```
@@ -178,27 +115,9 @@ export async function middleware(req: NextRequest) {
</TabPanel>
</Tabs>
We can now use our server-side Supabase client to fetch data in Server Components.
## Supabase Provider
```jsx
import 'server-only'
import { createClient } from '../../utils/supabase-server'
// do not cache this page
export const revalidate = 0
export default async function ServerComponent() {
const supabase = createClient()
const { data } = await supabase.from('posts').select('*')
return <pre>{JSON.stringify({ data }, null, 2)}</pre>
}
```
### Client-side
We still need a Supabase instance client-side for authentication and realtime subscriptions. It is important, when using Supabase client-side, to have a single instance of a client. We can share this singleton instance across our components using providers and React context.
All Client Components need to share a single instance of the Supabase client. We can wrap our application in a `<SupabaseProvider />` and use React Context to create a global Supabase instance.
<Tabs
scrollable
@@ -208,95 +127,32 @@ We still need a Supabase instance client-side for authentication and realtime su
>
<TabPanel id="js" label="JavaScript">
Create a new file at `/utils/supabase-browser.js` and populate with the following:
Create a new file at `/app/supabase-provider.jsx` and populate with the following:
```js title="/utils/supabase-browser.js"
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
export const createClient = () => createBrowserSupabaseClient()
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Create a new file at `/utils/supabase-browser.ts` and populate with the following:
```ts
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { Database } from '../lib/database.types'
export const createClient = () => createBrowserSupabaseClient<Database>()
```
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createBrowserSupabaseClient` to add type support to the Supabase client.
</TabPanel>
</Tabs>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Next, we need to create a single instance of Supabase to use client-side. Let's create a new Provider for Supabase at `/components/supabase-provider.jsx` and populate with the following:
```jsx title=components/supabase-provider.jsx
```jsx title="app/supabase-provider.jsx"
'use client'
import { createContext, useContext, useState } from 'react'
import { createClient } from '../utils/supabase-browser'
import { createContext, useContext, useEffect, useState } from 'react'
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { useRouter } from 'next/navigation'
const Context = createContext()
const Context = createContext(undefined)
export default function SupabaseProvider({ children }) {
const [supabase] = useState(() => createClient())
const [supabase] = useState(() => createBrowserSupabaseClient())
const router = useRouter()
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
if (session?.access_token !== accessToken) {
router.refresh()
}
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
router.refresh()
})
return () => subscription.unsubscribe()
}, [accessToken])
return (
<Context.Provider value={{ supabase }}>
<>{children}</>
</Context.Provider>
)
}
export const useSupabase = () => useContext(Context)
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Next, we need to create a single instance of Supabase to use client-side. Let's create a new Provider for Supabase at `/components/supabase-provider.tsx` and populate with the following:
```tsx title=components/supabase-provider.tsx
'use client'
import { createContext, useContext, useState } from 'react'
import { createClient } from '../utils/supabase-browser'
import type { SupabaseClient } from '@supabase/auth-helpers-nextjs'
import type { Database } from '../lib/database.types'
type SupabaseContext = {
supabase: SupabaseClient<Database>
}
const Context = createContext<SupabaseContext | undefined>(undefined)
export default function SupabaseProvider({ children }: { children: React.ReactNode }) {
const [supabase] = useState(() => createClient())
return () => {
subscription.unsubscribe()
}
}, [router, supabase])
return (
<Context.Provider value={{ supabase }}>
@@ -306,12 +162,69 @@ export default function SupabaseProvider({ children }: { children: React.ReactNo
}
export const useSupabase = () => {
let context = useContext(Context);
const context = useContext(Context)
if (context === undefined) {
throw new Error("useSupabase must be used inside SupabaseProvider");
} else {
return context;
throw new Error('useSupabase must be used inside SupabaseProvider')
}
return context
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Create a new file at `/app/supabase-provider.tsx` and populate with the following:
```tsx title="app/supabase-provider.tsx"
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { useRouter } from 'next/navigation'
import type { SupabaseClient } from '@supabase/auth-helpers-nextjs'
import type { Database } from '@/lib/database.types'
type SupabaseContext = {
supabase: SupabaseClient<Database>
}
const Context = createContext<SupabaseContext | undefined>(undefined)
export default function SupabaseProvider({ children }: { children: React.ReactNode }) {
const [supabase] = useState(() => createBrowserSupabaseClient())
const router = useRouter()
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
router.refresh()
})
return () => {
subscription.unsubscribe()
}
}, [router, supabase])
return (
<Context.Provider value={{ supabase }}>
<>{children}</>
</Context.Provider>
)
}
export const useSupabase = () => {
const context = useContext(Context)
if (context === undefined) {
throw new Error('useSupabase must be used inside SupabaseProvider')
}
return context
}
```
@@ -320,16 +233,6 @@ export const useSupabase = () => {
</TabPanel>
</Tabs>
We need to set up a listener to fetch fresh data whenever our user logs in or out. For this we need to check whether our client and server sessions match. Let's start by installing the `server-only` package.
```bash
npm install server-only
```
This will ensure that any component that imports this package will be a Server Component, and excluded from the browser bundle.
Next, let's modify our root layout to fetch the user's session, wrap our application in our Supabase Provider, and pass the server access token as a prop to the `<SupabaseListener />` component (we will create this next).
<Tabs
scrollable
size="small"
@@ -338,36 +241,22 @@ Next, let's modify our root layout to fetch the user's session, wrap our applica
>
<TabPanel id="js" label="JavaScript">
```jsx title=app/layout.jsx
import 'server-only'
Modify `layout.jsx` to wrap the application with the `<SupabaseProvider>` component:
import SupabaseListener from '../components/supabase-listener'
import SupabaseProvider from '../components/supabase-provider'
```jsx title="app/layout.jsx"
import './globals.css'
import { createClient } from '../utils/supabase-server'
import SupabaseProvider from './supabase-provider'
// do not cache this layout
export const revalidate = 0
export default async function RootLayout({ children }) {
const supabase = createClient()
const {
data: { session },
} = await supabase.auth.getSession()
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
{/*
<head /> will contain the components returned by the nearest parent
head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
*/}
<head />
<body>
<SupabaseProvider>
<SupabaseListener serverAccessToken={session?.access_token} />
{children}
</SupabaseProvider>
<SupabaseProvider>{children}</SupabaseProvider>
</body>
</html>
)
@@ -378,36 +267,22 @@ export default async function RootLayout({ children }) {
<TabPanel id="ts" label="TypeScript">
```tsx title=app/layout.tsx
import 'server-only'
Modify `layout.tsx` to wrap the application with the `<SupabaseProvider>` component:
import SupabaseListener from '../components/supabase-listener'
import SupabaseProvider from '../components/supabase-provider'
```tsx title="app/layout.tsx"
import './globals.css'
import { createClient } from '../utils/supabase-server'
import SupabaseProvider from './supabase-provider'
// do not cache this layout
export const revalidate = 0
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const supabase = createClient()
const {
data: { session },
} = await supabase.auth.getSession()
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
{/*
<head /> will contain the components returned by the nearest parent
head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
*/}
<head />
<body>
<SupabaseProvider>
<SupabaseListener serverAccessToken={session?.access_token} />
{children}
</SupabaseProvider>
<SupabaseProvider>{children}</SupabaseProvider>
</body>
</html>
)
@@ -417,7 +292,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</TabPanel>
</Tabs>
And now create our Supabase listener component that uses the singleton Supabase instance to listen for auth changes.
Now any of our Client Components can use the `useSupabase` hook to ensure they are using the same instance of a Supabase client.
## Creating a Supabase Client
### Client Components
While Server Components are great for data fetching, we still need to use Supabase client-side for [authentication](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login.tsx) and [realtime subscriptions](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx).
As mentioned above, it is important that all Client Components share a single instance of the Supabase client. We can use the `useSupabase` hook we created above to ensure this is the case.
<Tabs
scrollable
@@ -427,118 +310,24 @@ And now create our Supabase listener component that uses the singleton Supabase
>
<TabPanel id="js" label="JavaScript">
```jsx title="/components/supabase-listener.jsx"
```jsx title="app/new-post.jsx"
'use client'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useState } from 'react'
import { useSupabase } from './supabase-provider'
export default function SupabaseListener({ serverAccessToken }) {
export default function NewPost() {
const [content, setContent] = useState('')
const { supabase } = useSupabase()
const router = useRouter()
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
if (session?.access_token !== serverAccessToken) {
router.refresh()
}
})
return () => {
subscription.unsubscribe()
}
}, [serverAccessToken, router, supabase])
return null
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx title="/components/supabase-listener.tsx"
'use client'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useSupabase } from './supabase-provider'
export default function SupabaseListener({ serverAccessToken }: { serverAccessToken?: string }) {
const { supabase } = useSupabase()
const router = useRouter()
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
if (session?.access_token !== serverAccessToken) {
router.refresh()
}
})
return () => {
subscription.unsubscribe()
}
}, [serverAccessToken, router, supabase])
return null
}
```
</TabPanel>
</Tabs>
> `use client` tells Next.js that this is a [Client Component](https://beta.nextjs.org/docs/rendering/server-and-client-components#client-components). Only Client Components can use hooks like `useEffect` and `useRouter`.
The function we pass to `onAuthStateChange` is automatically called by Supabase whenever a user's session changes. This component takes an `serverAccessToken` prop, which is the server's state for our user. If the `serverAccessToken` and the new session's `access_token` do not match then the client and server are out of sync, therefore, we want to reload the active route.
Now we can use our `useSupabase` hook throughout our client-side components.
### Authentication
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx title="/components/login.jsx"
'use client'
import { useSupabase } from './supabase-provider'
// Supabase auth needs to be triggered client-side
export default function Login() {
const { supabase, session } = useSupabase()
const handleEmailLogin = async () => {
await supabase.auth.signInWithPassword({
email: 'jon@supabase.com',
password: 'password',
})
}
const handleGitHubLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: 'github',
})
}
const handleLogout = async () => {
await supabase.auth.signOut()
const handleSave = async () => {
const { data } = await supabase.from('posts').insert({ content }).select()
}
return (
<>
<button onClick={handleEmailLogin}>Email Login</button>
<button onClick={handleGitHubLogin}>GitHub Login</button>
<button onClick={handleLogout}>Logout</button>
<input onChange={(e) => setContent(e.target.value)} value={content} />
<button onClick={handleSave}>Save</button>
</>
)
}
@@ -548,37 +337,24 @@ export default function Login() {
<TabPanel id="ts" label="TypeScript">
```tsx title="/components/login.tsx"
```jsx title="app/new-post.tsx"
'use client'
import { useState } from 'react'
import { useSupabase } from './supabase-provider'
// Supabase auth needs to be triggered client-side
export default function Login() {
const { supabase, session } = useSupabase()
export default function NewPost() {
const [content, setContent] = useState('')
const { supabase } = useSupabase()
const handleEmailLogin = async () => {
await supabase.auth.signInWithPassword({
email: 'jon@supabase.com',
password: 'password',
})
}
const handleGitHubLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: 'github',
})
}
const handleLogout = async () => {
await supabase.auth.signOut()
const handleSave = async () => {
const { data } = await supabase.from('posts').insert({ content }).select()
}
return (
<>
<button onClick={handleEmailLogin}>Email Login</button>
<button onClick={handleGitHubLogin}>GitHub Login</button>
<button onClick={handleLogout}>Logout</button>
<input onChange={(e) => setContent(e.target.value)} value={content} />
<button onClick={handleSave}>Save</button>
</>
)
}
@@ -587,11 +363,11 @@ export default function Login() {
</TabPanel>
</Tabs>
### Realtime
> check out [this example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/supabase-provider.tsx) for making the user's session available to all Client Components.
A nice pattern for fetching data server-side and subscribing to changes client-side can be done by combining Server and Client components.
### Server Components
> To receive realtime events, you must [enable replication on your "posts" table in Supabase](https://app.supabase.com/project/_/database/replication).
In order to use Supabase in Server Components, you need to have implemented the `middleware.ts` steps above 👆
<Tabs
scrollable
@@ -601,57 +377,21 @@ A nice pattern for fetching data server-side and subscribing to changes client-s
>
<TabPanel id="js" label="JavaScript">
Create a new file at `/app/realtime/posts.jsx` and populate with the following:
```jsx title="/app/realtime/posts.jsx"
'use client'
import { useEffect, useState } from 'react'
import { useSupabase } from '../../components/supabase-provider'
export default function Posts({ serverPosts }) {
const [posts, setPosts] = useState(serverPosts)
const { supabase } = useSupabase()
useEffect(() => {
setPosts(serverPosts)
}, [serverPosts])
useEffect(() => {
const channel = supabase
.channel('*')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) =>
setPosts((posts) => [...posts, payload.new])
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [supabase, setPosts, posts])
return <pre>{JSON.stringify(posts, null, 2)}</pre>
}
```
This can now be used in a Server Component to subscribe to realtime updates.
Create a new file at `/app/realtime/page.jsx` and populate with the following:
```jsx title="/app/realtime/page.jsx"
import 'server-only'
import { createClient } from '../../utils/supabase-server'
import Posts from './posts'
```jsx title="app/page.jsx"
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { headers, cookies } from 'next/headers'
// do not cache this page
export const revalidate = 0
export default async function Realtime() {
const supabase = createClient()
export default async function ServerComponent() {
const supabase = createServerComponentSupabaseClient({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return <Posts serverPosts={data || []} />
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
```
@@ -659,69 +399,91 @@ export default async function Realtime() {
<TabPanel id="ts" label="TypeScript">
Create a new file at `/app/realtime/posts.tsx` and populate with the following:
```tsx title="app/page.tsx"
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { headers, cookies } from 'next/headers'
```tsx title="/app/realtime/posts.tsx"
'use client'
import { useEffect, useState } from 'react'
import { useSupabase } from '../../components/supabase-provider'
import type { Database } from '../../lib/database.types'
type Post = Database['public']['Tables']['posts']['Row']
export default function Posts({ serverPosts }: { serverPosts: Post[] }) {
const [posts, setPosts] = useState(serverPosts)
const { supabase } = useSupabase()
useEffect(() => {
setPosts(serverPosts)
}, [serverPosts])
useEffect(() => {
const channel = supabase
.channel('*')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) =>
setPosts((posts) => [...posts, payload.new as Post])
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [supabase, setPosts, posts])
return <pre>{JSON.stringify(posts, null, 2)}</pre>
}
```
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createServerSupabaseClient` to add type support to the Supabase client.
This can now be used in a Server Component to subscribe to realtime updates.
Create a new file at `/app/realtime/page.tsx` and populate with the following:
```tsx title="/app/realtime/page.tsx"
import 'server-only'
import { createClient } from '../../utils/supabase-server'
import Posts from './posts'
import type { Database } from '@/lib/database.types'
// do not cache this page
export const revalidate = 0
export default async function Realtime() {
const supabase = createClient()
export default async function ServerComponent() {
const supabase = createServerComponentSupabaseClient<Database>({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return <Posts serverPosts={data || []} />
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
```
</TabPanel>
</Tabs>
> check out [this example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/page.tsx) for redirecting unauthenticated users - protected pages.
### Route Handlers
In order to use Supabase in Route Handlers, you need to have implemented the `middleware.ts` steps above 👆
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx title="app/api/posts/route.jsx"
import { createRouteHandlerSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import { headers, cookies } from 'next/headers'
// do not cache this page
export const revalidate = 0
export async function GET() {
const supabase = createRouteHandlerSupabaseClient({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return NextResponse.json(data)
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx title="app/api/posts/route.tsx"
import { createRouteHandlerSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import { headers, cookies } from 'next/headers'
import type { Database } from '@/lib/database.types'
// do not cache this page
export const revalidate = 0
export async function GET() {
const supabase = createRouteHandlerSupabaseClient<Database>({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return NextResponse.json(data)
}
```
</TabPanel>
</Tabs>
> Check out [this repo](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) for a full example including [authentication](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login.tsx), [realtime](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx) and [protected pages](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/page.tsx).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -4,7 +4,7 @@ export const meta = {
id: 'nextjs',
title: 'Supabase Auth with Next.js',
description: 'Authentication helpers for Next.js API routes, middleware, and SSR.',
sidebar_label: 'Next.js',
sidebar_label: 'Next.js (pages)',
}
This submodule provides convenience helpers for implementing user authentication in Next.js applications.
@@ -86,7 +86,7 @@ import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
return {
session: getSession()
session: await getSession()
};
};
```
@@ -142,13 +142,15 @@ We need to create an event listener in the root `+layout.svelte` file in order c
export let data: LayoutData;
$: ({ supabase } = data);
$: ({ supabase, session } = data);
onMount(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
invalidate('supabase:auth');
} = supabase.auth.onAuthStateChange((event, _session) => {
if (_session?.expires_at !== session?.expires_at) {
invalidate('supabase:auth');
}
});
return () => subscription.unsubscribe();
@@ -185,26 +187,6 @@ declare global {
}
```
### Basic Setup
You can now determine if a user is authenticated on the client-side by checking that the `session` object in `$page.data` is defined.
```html
<!-- src/lib/components/Header.svelte -->
<script lang="ts">
import type { Session } from '@supabase/supabase-js';
export let session: Session | null;
</script>
{#if !session}
<h1>I am not logged in</h1>
{:else}
<h1>Welcome {session.user.email}</h1>
<p>I am logged in!</p>
{/if}
```
## Client-side data fetching with RLS
For [row level security](https://supabase.com/docs/guides/auth/row-level-security) to work properly when fetching data client-side, you need to use `supabaseClient` from `PageData` and only run your query once the session is defined client-side:
@@ -562,13 +544,15 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
export let data: LayoutData;
$: ({ supabase } = data);
$: ({ supabase, session } = data);
onMount(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
invalidate('supabase:auth');
} = supabase.auth.onAuthStateChange((event, _session) => {
if (_session?.expires_at !== session?.expires_at) {
invalidate('supabase:auth')
}
});
return () => subscription.unsubscribe();
@@ -0,0 +1,137 @@
import Layout from '~/layouts/DefaultGuideLayout'
import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
title: 'Use Supabase Auth with React',
subtitle: 'Learn how to Supabase Auth with React.js.',
breadcrumb: 'Auth Quickstarts',
}
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Create a new Supabase project">
[Launch a new project](https://app.supabase.com) in the Supabase Dashboard.
Your new database has a table for storing your users. You can see that this table is currently empty by running some SQL in the [SQL Editor](https://app.supabase.com/project/_/sql).
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sql SQL_EDITOR
select * from auth.users;
````
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Create a React app">
Create a React app using the `create-react-app` command.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash Terminal
npx create-react-app my-app
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Install the Supabase client library">
The fastest way to get started is to use Supabase's `auth-ui-react` library which provides a convenient interface for working with Supabase Auth from a React app.
Navigate to the React app and install the Supabase libraries.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash Terminal
cd my-app && npm install @supabase/supabase-js @supabase/auth-ui-react
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Set up your login component">
In `index.js`, create a Supabase client using your [Project URL and public API (anon) key](https://app.supabase.com/project/_/settings/api).
You can configure the Auth component to display whenever there is no session inside `supabase.auth.getSession()`
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```js src/index.js
import './index.css'
import { useState, useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>')
export default function App() {
const [session, setSession] = useState(null)
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session)
})
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
})
return () => subscription.unsubscribe()
}, [])
if (!session) {
return (<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} />)
}
else {
return (<div>Logged in!</div>)
}
}
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Start the app">
Start the app, go to http://localhost:3000 in a browser, and open the browser console and you should be able to log in.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash Terminal
npm start
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
export default Page
@@ -130,6 +130,9 @@ if (refreshToken && accessToken) {
await supabase.auth.setSession({
refresh_token: refreshToken,
access_token: accessToken,
{
auth: { persistSession: false },
}
})
} else {
// make sure you handle this case!
-404
View File
@@ -1,404 +0,0 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'api',
title: 'Serverless APIs',
description: 'Auto-generating and Realtime APIs.',
sidebar_label: 'Overview',
video: 'https://www.youtube.com/v/rPAJJFdtPw0',
}
Supabase auto-generates three types of API directly from your database schema.
- REST - interact with your database through a restful interface.
- Realtime - listen to database changes.
- GraphQL - [in beta](https://supabase.com/blog/pg-graphql).
The APIs are:
- **Instant and auto-generated.** <br />As you update your database the changes are immediately accessible through your API.
- **Self documenting.** <br />Supabase generates documentation in the Dashboard which updates as you make database changes.
- **Secure.** <br />The API is configured to work with PostgreSQL's Row Level Security, provisioned behind an API gateway with key-auth enabled.
- **Fast.** <br />Our benchmarks for basic reads are more than 300% faster than Firebase. The API is a very thin layer on top of Postgres, which does most of the heavy lifting.
- **Scalable.** <br />The API can serve thousands of simultaneous requests, and works well for Serverless workloads.
## REST API [#rest-api-overview]
Supabase provides a RESTful API using [PostgREST](https://postgrest.org/). This is a very thin API layer on top of Postgres.
It provides everything you need from a CRUD API:
- Basic CRUD operations
- Deeply nested joins, allowing you to fetch data from multiple tables in a single fetch
- Works with Postgres Views
- Works with Postgres Functions
- Works with the Postgres security model - including Row Level Security, Roles, and Grants.
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/rPAJJFdtPw0"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
## GraphQL API [#graphql-api-overview]
<Admonition type='note'>
GraphQL is in Beta, and may have breaking changes. It is only available on self-hosted setups and Supabase projects created after 28th March 2022.
</Admonition>
GraphQL in Supabase works through [pg_graphql](https://supabase.com/blog/pg-graphql), an open source PostgreSQL extension for GraphQL.
## Realtime API [#realtime-api-overview]
Supabase provides a Realtime API using [Realtime](https://github.com/supabase/realtime). You can use this to listen to database changes over websockets.
Realtime leverages PostgreSQL's built-in logical replication. You can manage your Realtime API simply by managing Postgres publications.
Go to your project's [Replication section](https://app.supabase.com/project/_/database/replication) to get started.
## Getting started
All APIs are auto-created from Database tables. After you have added tables or functions to your database, you can use the APIs provided.
### Creating API Routes
API routes are automatically created when you create Postgres Tables, Views, or Functions.
Let's create our first
API route by creating a table called `todos` to store tasks.
This creates a corresponding route `todos` which can accept `GET`, `POST`, `PATCH`, & `DELETE` requests.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Table editor](https://app.supabase.com/project/_/editor) page in the Dashboard.
1. Click **New Table** and create a table with the name `todos`.
1. Click **Save**.
1. Click **New Column** and create a column with the name `task` and type `text`.
1. Click **Save**.
<video width="99%" muted playsInline controls="true">
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
-- Create a table called "todos" with a column to store tasks.
create table todos (
id bigint generated by default as identity primary key,
task text check (char_length(task) > 3)
);
```
</TabPanel>
</Tabs>
### API URL and Keys
Every Supabase project has a unique API URL. Your API is secured behind an API gateway which requires an API Key for every request.
1. Go to the [Settings](https://app.supabase.com/project/_/settings/general) page in the Dashboard.
2. Click **API** in the sidebar.
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
<video width="99%" muted playsInline controls="true">
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
The REST API and the GraphQL API are both accessible through this URL:
- REST: `https://<project_ref>.supabase.co/rest/v1`
- GraphQL: `https://<project_ref>.supabase.co/graphql/v1`
Both of these routes require the `anon` key to be passed through an `apikey` header.
#### API Keys
You are provided with two keys:
- an `anon` key, which is safe to be used in a browser context.
- a `service_role` key, which should only be used on a server. This key can bypass Row Level Security. NEVER use this key in a browser.
### Accessing the docs in the Dashboard
#### REST API [#rest-api-dashboard-docs]
Supabase generates documentation in the [Dashboard](https://app.supabase.com) which updates as you make database changes.
Let's view the documentation for a `countries` table which we created in our database.
1. Go to the [API](https://app.supabase.com/project/_/api) page in the Dashboard.
2. Find the `countries` table under **Tables and Views** in the sidebar.
3. Switch between the JavaScript and the cURL docs using the tabs.
<video width="99%" muted playsInline controls="true">
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-docs.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
#### GraphQL
The GraphQL Endpoint that we provide (`https://<project_ref>.supabase.co/graphql/v1`) is compatible with any GraphiQL implementation that can pass an `apikey` header.
Some suggested applications:
- [paw.cloud](https://paw.cloud)
- [insomnia.rest](https://insomnia.rest)
- [postman.com/graphql](https://www.postman.com/graphql/)
- Self-hosted GraphiQL: GraphiQL can be served through a simple HTML file. See [this discussion](https://github.com/supabase/supabase/discussions/6144) for more details.
## Using the API
### REST API
You can interact with your API directly via HTTP requests, or you can use the client libraries which we provide.
Let's see how to make a request to the `todos` table which we created in the first step,
using the API URL (`SUPABASE_URL`) and Key (`SUPABASE_ANON_KEY`) we provided:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="javascript"
>
<TabPanel id="javascript" label="Javascript">
```javascript
// Initialize the JS client
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
// Make a request
const { data: todos, error } = await supabase.from('todos').select('*')
```
</TabPanel>
<TabPanel id="curl" label="cURL">
```bash
# Append /rest/v1/ to your URL, and then use the table name as the route
curl '<SUPABASE_URL>/rest/v1/todos' \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <SUPABASE_ANON_KEY>"
```
</TabPanel>
</Tabs>
JS Reference: [select()](/docs/reference/javascript/select),
[insert()](/docs/reference/javascript/insert),
[update()](/docs/reference/javascript/update),
[upsert()](/docs/reference/javascript/upsert),
[delete()](/docs/reference/javascript/delete),
[rpc()](/docs/reference/javascript/rpc) (call Postgres functions).
### GraphQL API
You can use any GraphQL client with the Supabase GraphQL API. For our GraphQL example we will use [urql](https://formidable.com/open-source/urql/docs/).
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="javascript"
>
<TabPanel id="javascript" label="Javascript">
```javascript
import { createClient, useQuery } from 'urql'
// Prepare API key and Authorization header
const headers = {
apikey: <SUPABASE_ANON_KEY>,
authorization: `Bearer ${<SUPABASE_ANON_KEY>}`,
}
// Create GraphQL client
// See: https://formidable.com/open-source/urql/docs/basics/react-preact/#setting-up-the-client
const client = createClient({
url: '<SUPABASE_URL>/graphql/v1',
fetchOptions: function createFetchOptions() {
return { headers }
},
})
// Prepare our GraphQL query
const TodosQuery = `
query {
todosCollection {
edges {
node {
id
title
}
}
}
}
`
// Query for the data (React)
const [result, reexecuteQuery] = useQuery({
query: TodosQuery,
})
// Read the result
const { data, fetching, error } = result
```
</TabPanel>
<TabPanel id="curl" label="cURL">
```bash
# Append /graphql/v1/ to your URL, and then use the table name as the route
curl --request POST '<SUPABASE_URL>/graphql/v1' \
-H 'apikey: <SUPABASE_ANON_KEY>' \
-H 'Authorization: Bearer <SUPABASE_ANON_KEY>' \
-H 'Content-Type: application/json' \
-d '{ "query":"{ todos(first: 3) { edges { node { id } } } }" }'
```
</TabPanel>
</Tabs>
### Realtime API
By default Realtime is disabled on your database. Let's turn on Realtime for the `todos` table.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Replication** in the sidebar.
3. Control which database events are sent by toggling **Insert**, **Update**, and **Delete**.
4. Control which tables broadcast changes by selecting **Source** and toggling each table.
<video width="99%" muted playsInline controls="true">
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-realtime.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
alter publication supabase_realtime add table todos;
```
</TabPanel>
</Tabs>
From the client, we can listen to any new data that is inserted into the `todos` table:
```javascript
// Initialize the JS client
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
// Create a function to handle inserts
const handleInserts = (payload) => {
console.log('Change received!', payload)
}
// Listen to inserts
const { data: todos, error } = await supabase.from('todos').on('INSERT', handleInserts).subscribe()
```
Use [subscribe()](/docs/reference/javascript/subscribe) to listen to database changes.
The Realtime API works through PostgreSQL's replication functionality. Postgres sends database changes to a [publication](/docs/guides/database/replication#publications)
called `supabase_realtime`, and by managing this publication you can control which data is broadcast.
## API Security
### Securing your Routes
Your API is designed to work with Postgres Row Level Security (RLS). If you use Supabase [Auth](/docs/guides/auth), you can restrict data based on the logged-in user.
To control access to your data, you can use [Policies](/docs/guides/auth#policies).
When you create a table in Postgres, Row Level Security is disabled by default. To enable RLS:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Authentication](https://app.supabase.com/project/_/auth/users) page in the Dashboard.
2. Click on **Policies** in the sidebar.
3. Select **Enable RLS** to enable Row Level Security.
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
alter table todos enable row level security;
```
</TabPanel>
</Tabs>
### The `service_role` key
Never expose the `service_role` key in a browser or anywhere where a user can see it. This Key is designed to bypass Row Level Security - so it should only be used on a private server.
A common use case for the `service_role` key is to run data analytics jobs on the backend. To support joins on user id, it is often useful to grant the service role read access to `auth.users` table.
```sql
grant select on table auth.users to service_role;
```
We have [partnered with GitHub](https://github.blog/changelog/2022-03-28-supabase-is-now-a-github-secret-scanning-partner/) to scan for Supabase `service_role` keys pushed to public repositories.
If they detect any keys with service_role privileges being pushed to GitHub, they will forward the API key to us, so that we can automatically revoke the detected secrets and notify you, protecting your data against malicious actors.
### Safeguards towards accidental deletes and updates
For all projects, by default, the Postgres extension [safeupdate](https://github.com/eradman/pg-safeupdate) is enabled for all queries coming from the API.
This ensures that any `delete()` or `update()` would fail if there are no accompanying filters provided.
To confirm that safeupdate is enabled for queries going through the API of your project, the following query could be run:
```sql
select usename, useconfig from pg_shadow where usename = 'authenticator';
```
The expected value for `useconfig` should be:
```sql
['session_preload_libraries=supautils, safeupdate']
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -149,35 +149,38 @@ Manage your projects programmatically. [Docs](/docs/reference/api).
Both Postgres and the Supabase Platform are production-ready. Some tools we offer on top of Postgres are still under development.
| Product | Feature | Stage |
| -------------------------- | ---------------------- | ------- |
| Database | Postgres | `GA` |
| Database | Triggers | `GA` |
| Database | Functions | `GA` |
| Database | Extensions | `GA` |
| Database | Full Text Search | `GA` |
| Database | Webhooks | `alpha` |
| Database | Point-in-Time Recovery | `alpha` |
| Database | Vault | `alpha` |
| Studio | | `GA` |
| Realtime | Postgres Changes | `GA` |
| Realtime | Broadcast | `beta` |
| Realtime | Presence | `beta` |
| Storage | | `GA` |
| Storage | S3 Backend | `GA` |
| Storage | CDN | `GA` |
| Storage | Smart CDN | `beta` |
| Storage | Image Transformations | `beta` |
| Edge Functions | | `beta` |
| Auth | OAuth Providers | `beta` |
| Auth | Passwordless | `beta` |
| Auth | Next.js Auth Helpers | `alpha` |
| Auth | SvelteKit Auth Helpers | `alpha` |
| Auth | Remix Auth Helpers | `alpha` |
| Management API | | `beta` |
| CLI | | `beta` |
| Client Library: JavaScript | | `GA` |
| Client Library: Dart | | `beta` |
| Product | Feature | Stage | Available on self-hosted |
| -------------------------- | ---------------------- | ------- | ------------------------------------------- |
| Database | Postgres | `GA` | ✅ |
| Database | Triggers | `GA` | ✅ |
| Database | Functions | `GA` | ✅ |
| Database | Extensions | `GA` | ✅ |
| Database | Full Text Search | `GA` | ✅ |
| Database | Webhooks | `GA` | ✅ |
| Database | Point-in-Time Recovery | `alpha` | 🚧 [wal-g](https://github.com/wal-g/wal-g) |
| Database | Vault | `alpha` | ✅ |
| Studio | | `GA` | ✅ |
| Realtime | Postgres Changes | `GA` | ✅ |
| Realtime | Broadcast | `beta` | ✅ |
| Realtime | Presence | `beta` | ✅ |
| Storage | | `GA` | ✅ |
| Storage | S3 Backend | `GA` | ✅ |
| Storage | CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
| Storage | Smart CDN | `beta` | 🚧 [Cloudflare](https://www.cloudflare.com) |
| Storage | Image Transformations | `beta` | ✅ |
| Edge Functions | | `beta` | 🚧 [Deno Deploy](https://deno.com/deploy) |
| Auth | OAuth Providers | `beta` | ✅ |
| Auth | Passwordless | `beta` | ✅ |
| Auth | Next.js Auth Helpers | `alpha` | ✅ |
| Auth | SvelteKit Auth Helpers | `alpha` | ✅ |
| Auth | Remix Auth Helpers | `alpha` | ✅ |
| Management API | | `beta` | N/A |
| CLI | | `beta` | N/A |
| Client Library: JavaScript | | `GA` | N/A |
| Client Library: Dart | | `beta` | N/A |
- ✅ = Fully Available
- 🚧 = Available, but requires external tools or configuration
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -36,6 +36,7 @@ Then let's install the additional dependencies: [supabase-js](https://github.com
```bash
npm install @supabase/supabase-js
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
npx expo install expo-secure-store
```
Now let's create a helper file to initialize the Supabase client.
@@ -44,15 +45,28 @@ These variables will be exposed on the browser, and that's completely fine since
[Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
```ts title=lib/supabase.ts
import AsyncStorage from '@react-native-async-storage/async-storage'
import * as SecureStore from "expo-secure-store";
import { createClient } from '@supabase/supabase-js'
const ExpoSecureStoreAdapter = {
getItem: (key: string) => {
return SecureStore.getItemAsync(key);
},
setItem: (key: string, value: string) => {
SecureStore.setItemAsync(key, value);
},
removeItem: (key: string) => {
SecureStore.deleteItemAsync(key);
},
};
const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: AsyncStorage as any,
storage: ExpoSecureStoreAdapter as any,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
@@ -318,6 +318,8 @@ export default function Account({ session }) {
Let's create a new component for that called `Account.tsx` within a `components` folder.
First, [generate type definitions from your database using the Supabase CLI](https://supabase.com/docs/guides/database/api/generating-types) to `../utils/database.types`.
```tsx title=components/Account.tsx
import { useState, useEffect } from 'react'
import { useUser, useSupabaseClient, Session } from '@supabase/auth-helpers-react'
@@ -179,11 +179,13 @@ Update your `src/routes/+layout.svelte`:
export let data: LayoutData
$: ({ supabase } = data)
$: ({ supabase, session } = data)
onMount(() => {
const { data } = supabase.auth.onAuthStateChange(() => {
invalidate('supabase:auth')
const { data } = supabase.auth.onAuthStateChange((event, _session) => {
if (_session?.expires_at !== session?.expires_at) {
invalidate('supabase:auth')
}
})
return () => data.subscription.unsubscribe()
@@ -1,105 +0,0 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'dashibase',
title: 'Dashibase',
description: 'Create an internal CRUD dashboard with Supabase and Dashibase in 15 mins.',
}
Are you tired of building admin panels? [Dashibase](https://dashibase.com/) is a super fast way to build internal CRUD dashboards with a Notion-like UI.
This guide walks you through how to build a customer admin panel using Supabase on the backend and Dashibase on the front so that you and your team can easily find and update customer information.
## Quick explanation
Just so we are on the same page, here are some of the terms we use:
- **Data source:** This is the data that powers your dashboard (i.e. your Supabase database).
- **Dashboard:** This is the friendly interface that you build on top of your Supabase database using Dashibase.
- **Page:** Each dashboard can have multiple pages. Each page can have multiple blocks of text or tables.
Now that is out of the way, let's build our customer admin panel.
(Start your stopwatch!)
## Step 1. Connect Supabase to Dashibase
First, in Dashibase, go to “Data Sources” via the left sidebar and click on “Connect data source” in the upper-right corner.
Note: We do not download or store any of your database data.
![Connect Supabase to Dashibase](/docs/img/guides/integrations/dashibase/connect-database.png)
Enter your database credentials, which you can find in Supabase under Settings > Database > Connection info. For additional security, you can also download your SSL certificate and upload it to Dashibase.
![Supabase credentials](/docs/img/guides/integrations/dashibase/supabase-credentials.png)
## Step 2. Set up your data access policy
Once you have connected your database, you can also control what your team can see and edit in your database via Dashibase. There are usually certain data that you don’t want others to edit or even see (e.g. address, email, identification number).
Note: If you prefer to restrict access to your database _before_ you connect to Dashibase for security reasons, you can [create a restricted user](https://aws.amazon.com/blogs/database/managing-postgresql-users-and-roles/) via [Supabase's SQL Editor](/docs/guides/database#the-sql-editor). The slight advantage of doing this in Dashibase is that there is a GUI, which makes it easier to set up the access control (but we understand some developers might find it more secure to restrict the access themselves).
![Set data access in Dashibase](/docs/img/guides/integrations/dashibase/set-data-access.png)
You can even control which columns within a table that others can see and edit. In the example below, for the `tags` table, the `name` column can be read and updated, the `created_at` column can be read, and the `id` column cannot even be read.
![Set column-level access in Dashibase](/docs/img/guides/integrations/dashibase/set-column-access.png)
Once you are happy with the data access control, you can decide who in your organization can edit this data source (usually other developers) or use this data source to create dashboards.
![Set user access to data source in Dashibase](/docs/img/guides/integrations/dashibase/data-source-user-access.png)
## Step 3. Build your dashboard
To create a dashboard, go to “Dashboards” via the left sidebar in Dashibase and click on “Create dashboard” in the upper-right corner.
Here’s where the fun begins. There are many ways to build your dashboards in Dashibase using features such as markdown formatting and slash command.
You will see some automatically-generated content in your dashboard so that you can set up your dashboard more quickly. For this example, let's clear them out and start from scratch.
![Automatically-generated content for your dashboard](/docs/img/guides/integrations/dashibase/initial-dashboard-content.png)
Using our Notion-like UI, you can quickly build out your dashboard through typing. For example, you can type '/table' to add a table from your database to your dashboard. No more dragging components across your screen and dropping them on a blank canvas repeatedly.
![Automatically-generated content for your dashboard](/docs/img/guides/integrations/dashibase/slash-command.gif)
If you have set up [foreign keys in your Supabase database](/docs/guides/database/tables#joining-tables-with-foreign-keys), you can easily show data from multiple tables on the table in your Dashibase dashboard. Simply click on the "Columns" button beside the "Filter" button and select the columns you want to show or hide. For this example, we want to show the customers' country and plan price, which are in separate `countries` and `plans` tables.
![Setting up the Customers table](/docs/img/guides/integrations/dashibase/customers-table.png)
For all the tables in your dashboard, you can rename the column headers, add filter and sort, adjust the column width, and more.
You can also add text to provide context or instructions for the rest of your team as they are using your dashboard. This saves you the trouble of having your dashboard and documentation in separate places.
![Customers table with instructions](/docs/img/guides/integrations/dashibase/customers-table-with-text.png)
Most importantly, we automatically generate a single-item view for each of the items on your table so that you do not have to manually create it yourself. You can easily edit or rearrange the blocks just like in Notion.
![Automatically generated single-item view](/docs/img/guides/integrations/dashibase/single-item-view.png)
## Step 4. Share your dashboard with your team
Finally, save and share the dashboard with your team. There are two levels of access:
- Edit dashboard: Edit the text blocks, edit the tables, etc.
- Use dashboard: View and update the data via the dashboard
Click on the “Share” button in the upper-right corner of your dashboard to share access:
![Share dashboard access](/docs/img/guides/integrations/dashibase/share-dashboard.png)
Now your team can use this customer admin panel to find and update customer information easily!
P.S. How long did we take? :)
## Resources
- [Dashibase website](https://dashibase.com/)
- [Dashibase open-source repo](https://github.com/dashibase)
- [Dashibase Twitter](https://twitter.com/dashibase)
- [Dashibase Discord](https://discord.gg/crhDFYvbbq)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -7,12 +7,6 @@ export const meta = {
video: 'https://www.youtube.com/v/6rcGnW_Mh-0',
}
<Admonition type="note">
Custom Domains are currently in beta, and are being slowly made available to projects.
</Admonition>
Custom domains allow you to present a branded experience to your users. Custom domains are available as a [add-on for projects on a paid tier](https://app.supabase.com/project/_/settings/billing/update). Setting up a custom domain requires [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. Currently, you must use a subdomain (e.g., `api.example.com`, rather than `example.com`) for the purposes of this guide.
<div className="video-container">
@@ -99,12 +93,6 @@ As with the final activation stage of the process for setting up a custom domain
## Vanity Subdomains
<Admonition type="note">
Vanity Subdomains are currently in beta, and are being slowly made available to projects. [Contact Support](https://app.supabase.com/support/new) if you'd like to request early access.
</Admonition>
Vanity Subdomains allow you to present a basic branded experience, compared to custom domains. They allow you to host your services at a custom subdomain on Supabase (e.g., `my-example-brand.supabase.co`) instead of the default, randomly-assigned `foobarbaz.supabase.co`.
To get started:
@@ -335,7 +335,8 @@ const channel = supabase
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.
The `apikey` in `headers` must be either the `anon` or `service_role` token that Supabase provides for every project.
You can find these tokens under [Project API keys](https://app.supabase.com/project/_/settings/api) in your project's dashboard.
This will authenticate your request in the API gateway.
<Admonition type="caution">
+1
View File
@@ -54,6 +54,7 @@ We recommend installing all extensions into an `extensions` schema. This will ke
since all tables in the `public` schema are exposed via the API.
```sql
{/* prettier-ignore */}
create schema if not exists extensions;
create extension if not exists "uuid-ossp" with schema extensions;
create extension if not exists pgcrypto with schema extensions;
-6
View File
@@ -426,12 +426,6 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/integrations/dashibase</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/integrations/dhiwise</loc>
<changefreq>weekly</changefreq>
-962
View File
@@ -1,962 +0,0 @@
// ----------------
// 1. CSS Variables
// 2. Dark Mode
// 3. Autocomplete
// 4. Panel
// 5. Sources
// 6. Hit Layout
// 7. Panel Header
// 8. Panel Footer
// 9. Detached Mode
// 10. Gradients
// 11. Utilities
// ----------------
// Note:
// This theme reflects the markup structure of autocomplete with SCSS indentation.
// We use the SASS `@at-root` function to keep specificity low.
// ----------------
// 1. CSS Variables
// ----------------
:root {
// Input
--aa-search-input-height: 38px;
--aa-input-icon-size: 20px;
// Size and spacing
--aa-base-unit: 16;
--aa-spacing-factor: 1;
--aa-spacing: calc(var(--aa-base-unit) * var(--aa-spacing-factor) * 1px);
--aa-spacing-half: calc(var(--aa-spacing) / 2);
--aa-panel-max-height: 650px;
// Z-index
--aa-base-z-index: 9999;
// Font
--aa-font-size: calc(var(--aa-base-unit) * 1px);
--aa-font-family: inherit;
--aa-font-weight-medium: 500;
--aa-font-weight-semibold: 600;
--aa-font-weight-bold: 700;
// Icons
--aa-icon-size: 20px;
--aa-icon-stroke-width: 1.6;
--aa-icon-color-rgb: 119, 119, 163;
--aa-icon-color-alpha: 1;
--aa-action-icon-size: 20px;
// Text colors
--aa-text-color-rgb: 38, 38, 39;
--aa-text-color-alpha: 1;
--aa-primary-color-rgb: 62, 52, 211;
--aa-primary-color-alpha: 0.2;
--aa-muted-color-rgb: 128, 126, 163;
--aa-muted-color-alpha: 0.6;
// Border colors
--aa-panel-border-color-rgb: 128, 126, 163;
--aa-panel-border-color-alpha: 0.3;
--aa-input-border-color-rgb: 128, 126, 163;
--aa-input-border-color-alpha: 0.8;
// Background colors
--aa-background-color-rgb: 255, 255, 255;
--aa-background-color-alpha: 1;
--aa-input-background-color-rgb: 255, 255, 255;
--aa-input-background-color-alpha: 1;
--aa-selected-color-rgb: 179, 173, 214;
--aa-selected-color-alpha: 0.205;
--aa-description-highlight-background-color-rgb: 245, 223, 77;
--aa-description-highlight-background-color-alpha: 0.5;
// Detached mode
--aa-detached-media-query: (max-width: 680px);
--aa-detached-modal-media-query: (min-width: 680px);
--aa-detached-modal-max-width: 680px;
--aa-detached-modal-max-height: 500px;
--aa-overlay-color-rgb: 115, 114, 129;
--aa-overlay-color-alpha: 0.4;
// Shadows
--aa-panel-shadow: 0 0 0 1px rgba(35, 38, 59, 0.1), 0 6px 16px -4px rgba(35, 38, 59, 0.15);
// Scrollbar
--aa-scrollbar-width: 13px;
--aa-scrollbar-track-background-color-rgb: 234, 234, 234;
--aa-scrollbar-track-background-color-alpha: 1;
--aa-scrollbar-thumb-background-color-rgb: var(--aa-background-color-rgb);
--aa-scrollbar-thumb-background-color-alpha: 1;
// Touch screens
@media (hover: none) and (pointer: coarse) {
--aa-spacing-factor: 1.2;
--aa-action-icon-size: 22px;
}
}
// ----------------
// 2. Dark Mode
// ----------------
body {
/* stylelint-disable selector-no-qualifying-type, selector-class-pattern */
&[data-theme='dark'],
&.dark {
// Text colors
--aa-text-color-rgb: 183, 192, 199;
--aa-primary-color-rgb: 146, 138, 255;
--aa-muted-color-rgb: 146, 138, 255;
// Background colors
--aa-input-background-color-rgb: 0, 3, 9;
--aa-background-color-rgb: 21, 24, 42;
--aa-selected-color-rgb: 146, 138, 255;
--aa-selected-color-alpha: 0.25;
--aa-description-highlight-background-color-rgb: 0 255 255;
--aa-description-highlight-background-color-alpha: 0.25;
// Icons
--aa-icon-color-rgb: 119, 119, 163;
// Shadows
--aa-panel-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9);
// Scrollbar
--aa-scrollbar-track-background-color-rgb: 44, 46, 64;
--aa-scrollbar-thumb-background-color-rgb: var(--aa-background-color-rgb);
}
/* stylelint-enable selector-no-qualifying-type, selector-class-pattern */
}
// Reset for `@extend`
%reset {
box-sizing: border-box;
}
// Init for `@extend`
%init {
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
font-family: var(--aa-font-family);
font-size: var(--aa-font-size);
font-weight: normal;
line-height: 1em;
margin: 0;
padding: 0;
text-align: left;
}
// ----------------
// 3. Autocomplete
// ----------------
.aa-Autocomplete,
.aa-DetachedFormContainer {
@extend %init;
* {
@extend %reset;
}
// Search box
@at-root .aa-Form {
@apply transition bg-scale-200 border border-scale-500;
align-items: center;
border-radius: 3px;
display: flex;
line-height: 1em;
margin: 0;
position: relative;
width: 100%;
&:focus-within {
@apply border-brand-900;
outline: currentColor none medium;
}
@at-root .aa-InputWrapperPrefix {
align-items: center;
display: flex;
flex-shrink: 0;
height: var(--aa-search-input-height);
order: 1;
// Container for search and loading icons
@at-root .aa-Label,
.aa-LoadingIndicator {
cursor: initial;
flex-shrink: 0;
height: 100%;
padding: 0;
text-align: left;
svg {
@apply text-brand-900;
height: auto;
max-height: var(--aa-input-icon-size);
stroke-width: var(--aa-icon-stroke-width);
width: var(--aa-input-icon-size);
}
}
@at-root .aa-SubmitButton,
.aa-LoadingIndicator {
height: 100%;
padding-left: calc(var(--aa-spacing) * 0.75 - 1px);
padding-right: var(--aa-spacing-half);
width: calc(var(--aa-spacing) * 1.75 + var(--aa-icon-size) - 1px);
@media (hover: none) and (pointer: coarse) {
padding-left: calc(var(--aa-spacing-half) / 2 - 1px);
width: calc(var(--aa-icon-size) + (var(--aa-spacing) * 1.25) - 1px);
}
}
@at-root .aa-SubmitButton {
appearance: none;
background: none;
border: 0;
margin: 0;
}
@at-root .aa-LoadingIndicator {
align-items: center;
display: flex;
justify-content: center;
&[hidden] {
display: none;
}
}
}
@at-root .aa-InputWrapper {
order: 3;
position: relative;
width: 100%;
// Search box input (with placeholder and query)
@at-root .aa-Input {
@apply text-sm text-scale-1200;
appearance: none;
background: none;
border: 0;
height: var(--aa-search-input-height);
padding: 0;
width: 100%;
&::placeholder {
@apply text-scale-700;
opacity: 1;
}
// Focus is set and styled on the parent, it isn't necessary here
&:focus {
border-color: none;
box-shadow: none;
outline: none;
}
// Remove native appearence
&::-webkit-search-decoration,
&::-webkit-search-cancel-button,
&::-webkit-search-results-button,
&::-webkit-search-results-decoration {
appearance: none;
}
}
}
@at-root .aa-InputWrapperSuffix {
align-items: center;
display: flex;
height: var(--aa-search-input-height);
order: 4;
// Accelerator to clear the query
@at-root .aa-ClearButton {
align-items: center;
background: none;
border: 0;
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
cursor: pointer;
display: flex;
height: 100%;
margin: 0;
padding: 0 calc(var(--aa-spacing) * (5 / 6) - 0.5px);
@media (hover: none) and (pointer: coarse) {
padding: 0 calc(var(--aa-spacing) * (2 / 3) - 0.5px);
}
&:hover,
&:focus {
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
}
&[hidden] {
display: none;
}
svg {
stroke-width: var(--aa-icon-stroke-width);
width: var(--aa-icon-size);
}
}
}
}
}
// ----------------
// 4. Panel
// ----------------
.aa-Panel {
@extend %init;
@apply bg-scale-200;
border-radius: calc(var(--aa-spacing) / 4);
box-shadow: var(--aa-panel-shadow);
margin: 8px 0 0;
overflow: hidden;
position: absolute;
transition: opacity 200ms ease-in, filter 200ms ease-in;
@media screen and (prefers-reduced-motion) {
transition: none;
}
* {
@extend %reset;
}
button {
appearance: none;
background: none;
border: 0;
margin: 0;
padding: 0;
}
@at-root .aa-PanelLayout {
@apply bg-scale-200;
height: 100%;
margin: 0;
max-height: var(--aa-panel-max-height);
overflow-y: auto;
padding: 0;
position: relative;
text-align: left;
@at-root .aa-PanelLayoutColumns--twoGolden {
display: grid;
grid-template-columns: 39.2% auto;
overflow: hidden;
padding: 0;
}
@at-root .aa-PanelLayoutColumns--two {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
overflow: hidden;
padding: 0;
}
@at-root .aa-PanelLayoutColumns--three {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
overflow: hidden;
padding: 0;
}
}
// When a request isn't resolved yet
@at-root .aa-Panel--stalled {
.aa-Source {
filter: grayscale(1);
opacity: 0.8;
}
}
@at-root .aa-Panel--scrollable {
margin: 0;
max-height: var(--aa-panel-max-height);
overflow-x: hidden;
overflow-y: auto;
padding: var(--aa-spacing-half);
scrollbar-color: rgba(
var(--aa-scrollbar-thumb-background-color-rgb),
var(--aa-scrollbar-thumb-background-color-alpha)
)
rgba(
var(--aa-scrollbar-track-background-color-rgb),
var(--aa-scrollbar-track-background-color-alpha)
);
scrollbar-width: thin;
&::-webkit-scrollbar {
width: var(--aa-scrollbar-width);
}
&::-webkit-scrollbar-track {
background-color: rgba(
var(--aa-scrollbar-track-background-color-rgb),
var(--aa-scrollbar-track-background-color-alpha)
);
}
&::-webkit-scrollbar-thumb {
background-color: rgba(
var(--aa-scrollbar-thumb-background-color-rgb),
var(--aa-scrollbar-thumb-background-color-alpha)
);
border-color: rgba(
var(--aa-scrollbar-track-background-color-rgb),
var(--aa-scrollbar-track-background-color-alpha)
);
border-radius: 9999px;
border-style: solid;
border-width: 3px 2px 3px 3px;
}
}
}
// ----------------
// 5. Sources
// Each source can be styled independently
// ----------------
.aa-Source {
margin: 0;
padding: 0;
position: relative;
width: 100%;
&:empty {
// Hide empty section
display: none;
}
@at-root .aa-SourceNoResults {
font-size: 1em;
margin: 0;
padding: var(--aa-spacing);
}
// List of results inside the source
@at-root .aa-List {
list-style: none;
margin: 0;
padding: 0;
position: relative;
}
// Source title
@at-root .aa-SourceHeader {
margin: var(--aa-spacing-half) 0.5em var(--aa-spacing-half) 0;
padding: 0;
position: relative;
// Hide empty header
&:empty {
display: none;
}
// Title typography
@at-root .aa-SourceHeaderTitle {
background: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
color: rgba(var(--aa-primary-color-rgb), 1);
display: inline-block;
font-size: 0.8em;
font-weight: var(--aa-font-weight-semibold);
margin: 0;
padding: 0 var(--aa-spacing-half) 0 0;
position: relative;
z-index: var(--aa-base-z-index);
}
// Line separator
@at-root .aa-SourceHeaderLine {
border-bottom: solid 1px rgba(var(--aa-primary-color-rgb), 1);
display: block;
height: 2px;
left: 0;
margin: 0;
opacity: 0.3;
padding: 0;
position: absolute;
right: 0;
top: var(--aa-spacing-half);
z-index: calc(var(--aa-base-z-index) - 1);
}
}
// See all button
@at-root .aa-SourceFooterSeeAll {
background: linear-gradient(
180deg,
rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha)),
rgba(128, 126, 163, 0.14)
);
border: 1px solid rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
border-radius: 5px;
box-shadow: inset 0 0 2px #fff, 0 2px 2px -1px rgba(76, 69, 88, 0.15);
color: inherit;
font-size: 0.95em;
font-weight: var(--aa-font-weight-medium);
padding: 0.475em 1em 0.6em;
text-decoration: none;
&:focus,
&:hover {
border: 1px solid rgba(var(--aa-primary-color-rgb), 1);
color: rgba(var(--aa-primary-color-rgb), 1);
}
}
}
// ----------------
// 6. Hit Layout
// ----------------
.aa-Item {
@apply px-4 py-4;
align-items: center;
border-radius: 3px;
cursor: pointer;
display: grid;
min-height: calc(var(--aa-spacing) * 2.5);
// When the result is active
&[aria-selected='true'] {
@apply bg-scale-400;
.aa-ItemActionButton,
.aa-ActiveOnly {
visibility: visible;
}
}
// The result type icon inlined SVG or image
@at-root .aa-ItemIcon {
align-items: center;
background: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
border-radius: 3px;
box-shadow: inset 0 0 0 1px
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
color: rgba(var(--aa-icon-color-rgb), var(--aa-icon-color-alpha));
display: flex;
flex-shrink: 0;
font-size: 0.7em;
height: calc(var(--aa-icon-size) + var(--aa-spacing-half));
justify-content: center;
overflow: hidden;
stroke-width: var(--aa-icon-stroke-width);
text-align: center;
width: calc(var(--aa-icon-size) + var(--aa-spacing-half));
img {
height: auto;
max-height: calc(var(--aa-icon-size) + var(--aa-spacing-half) - 8px);
max-width: calc(var(--aa-icon-size) + var(--aa-spacing-half) - 8px);
width: auto;
}
svg {
height: var(--aa-icon-size);
width: var(--aa-icon-size);
}
@at-root .aa-ItemIcon--alignTop {
align-self: flex-start;
}
@at-root .aa-ItemIcon--noBorder {
background: none;
box-shadow: none;
}
@at-root .aa-ItemIcon--picture {
height: 96px;
width: 96px;
img {
max-height: 100%;
max-width: 100%;
padding: var(--aa-spacing-half);
}
}
}
@at-root .aa-ItemContent {
@apply flex flex-col space-y-1;
cursor: pointer;
line-height: 1.25em;
overflow: hidden;
&:empty {
display: none;
}
mark {
@apply text-brand-900;
background: none;
font-style: normal;
font-weight: var(--aa-font-weight-bold);
}
.aa-ItemTitle {
@apply text-scale-1200 text-sm;
}
@at-root .aa-ItemContent--dual {
display: flex;
flex-direction: column;
justify-content: space-between;
text-align: left;
.aa-ItemContentTitle,
.aa-ItemContentSubtitle {
display: block;
}
}
@at-root .aa-ItemContent--indented {
padding-left: calc(var(--aa-icon-size) + var(--aa-spacing));
}
@at-root .aa-ItemContentBody {
display: grid;
gap: calc(var(--aa-spacing-half) / 2);
}
@at-root .aa-ItemContentTitle {
display: inline-block;
margin: 0 0.5em 0 0;
max-width: 100%;
overflow: hidden;
padding: 0;
text-overflow: ellipsis;
white-space: nowrap;
}
@at-root .aa-ItemContentSubtitle {
@apply text-scale-1000 text-sm truncate;
@at-root .aa-ItemContentSubtitleIcon {
&::before {
border-color: rgba(var(--aa-muted-color-rgb), 0.64);
border-style: solid;
content: '';
display: inline-block;
left: 1px;
position: relative;
top: -3px;
}
}
@at-root .aa-ItemContentSubtitle--inline {
.aa-ItemContentSubtitleIcon {
&::before {
border-width: 0 0 1.5px;
margin-left: var(--aa-spacing-half);
margin-right: calc(var(--aa-spacing-half) / 2);
width: calc(var(--aa-spacing-half) + 2px);
}
}
}
@at-root .aa-ItemContentSubtitle--standalone {
align-items: center;
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
display: grid;
gap: var(--aa-spacing-half);
grid-auto-flow: column;
justify-content: start;
.aa-ItemContentSubtitleIcon {
&::before {
border-radius: 0 0 0 3px;
border-width: 0 0 1.5px 1.5px;
height: var(--aa-spacing-half);
width: var(--aa-spacing-half);
}
}
}
@at-root .aa-ItemContentSubtitleCategory {
color: rgba(var(--aa-muted-color-rgb), 1);
font-weight: 500;
}
}
@at-root .aa-ItemContentDescription {
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
font-size: 0.85em;
max-width: 100%;
overflow-x: hidden;
text-overflow: ellipsis;
&:empty {
display: none;
}
mark {
background: rgba(
var(--aa-description-highlight-background-color-rgb),
var(--aa-description-highlight-background-color-alpha)
);
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
font-style: normal;
font-weight: var(--aa-font-weight-medium);
}
}
@at-root .aa-ItemContentDash {
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
display: none;
opacity: 0.4;
}
@at-root .aa-ItemContentTag {
background-color: rgba(var(--aa-primary-color-rgb), var(--aa-primary-color-alpha));
border-radius: 3px;
margin: 0 0.4em 0 0;
padding: 0.08em 0.3em;
}
}
// wrap hit with url but we don't need to see it
@at-root .aa-ItemWrapper,
.aa-ItemLink {
align-items: center;
color: inherit;
gap: calc(var(--aa-spacing-half) / 2);
grid-auto-flow: column;
justify-content: space-between;
width: 100%;
}
@at-root .aa-ItemLink {
color: inherit;
text-decoration: none;
}
// Secondary click actions
@at-root .aa-ItemActions {
display: grid;
grid-auto-flow: column;
height: 100%;
justify-self: end;
margin: 0 calc(var(--aa-spacing) / -3);
padding: 0 2px 0 0;
}
@at-root .aa-ItemActionButton {
align-items: center;
background: none;
border: 0;
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
cursor: pointer;
display: flex;
flex-shrink: 0;
padding: 0;
&:hover,
&:focus {
svg {
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
@media (hover: none) and (pointer: coarse) {
color: inherit;
}
}
}
svg {
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
margin: 0;
margin: calc(var(--aa-spacing) / 3);
stroke-width: var(--aa-icon-stroke-width);
width: var(--aa-action-icon-size);
}
}
@at-root .aa-ActiveOnly {
visibility: hidden;
}
}
//----------------
// 7. Panel Header
//----------------
.aa-PanelHeader {
align-items: center;
background: rgba(var(--aa-primary-color-rgb), 1);
color: #fff;
display: grid;
height: var(--aa-modal-header-height);
margin: 0;
padding: var(--aa-spacing-half) var(--aa-spacing);
position: relative;
&::after {
background-image: linear-gradient(
rgba(var(--aa-background-color-rgb), 1),
rgba(var(--aa-background-color-rgb), 0)
);
bottom: calc(var(--aa-spacing-half) * -1);
content: '';
height: var(--aa-spacing-half);
left: 0;
pointer-events: none;
position: absolute;
right: 0;
z-index: var(--aa-base-z-index);
}
}
//----------------
// 8. Panel Footer
//----------------
.aa-PanelFooter {
background-color: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
box-shadow: inset 0 1px 0
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
display: flex;
justify-content: space-between;
margin: 0;
padding: var(--aa-spacing);
position: relative;
z-index: var(--aa-base-z-index);
&::after {
background-image: linear-gradient(
rgba(var(--aa-background-color-rgb), 0),
rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha))
);
content: '';
height: var(--aa-spacing);
left: 0;
opacity: 0.12;
pointer-events: none;
position: absolute;
right: 0;
top: calc(var(--aa-spacing) * -1);
z-index: calc(var(--aa-base-z-index) - 1);
}
}
//----------------
// 9. Detached Mode
//----------------
.aa-DetachedContainer {
@apply bg-scale-200 border border-scale-500;
bottom: 0;
box-shadow: var(--aa-panel-shadow);
display: flex;
flex-direction: column;
left: 0;
margin: 0;
overflow: hidden;
padding: 0;
position: fixed;
right: 0;
top: 0;
z-index: var(--aa-base-z-index);
&::after {
height: 32px;
}
.aa-SourceHeader {
margin: var(--aa-spacing-half) 0 var(--aa-spacing-half) 2px;
}
.aa-Panel {
@apply bg-scale-200;
border-radius: 0;
box-shadow: none;
flex-grow: 1;
margin: 0;
padding: 0;
position: relative;
}
.aa-PanelLayout {
bottom: 0;
box-shadow: none;
left: 0;
margin: 0;
max-height: none;
overflow-y: auto;
position: absolute;
right: 0;
top: 0;
width: 100%;
}
@at-root .aa-DetachedFormContainer {
@apply bg-scale-200;
border-bottom: solid 1px
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
display: flex;
flex-direction: row;
justify-content: space-between;
margin: 0;
padding: var(--aa-spacing-half);
@at-root .aa-DetachedCancelButton {
@apply text-scale-1100 text-sm;
background: none;
border: 0;
border-radius: 3px;
cursor: pointer;
margin: 0 0 0 var(--aa-spacing-half);
padding: 0 var(--aa-spacing-half);
&:hover,
&:focus {
box-shadow: inset 0 0 0 1px
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
}
}
}
@at-root .aa-DetachedContainer--modal {
border-radius: 6px;
bottom: inherit;
height: auto;
margin: 0 auto;
max-width: var(--aa-detached-modal-max-width);
position: absolute;
top: 3%;
.aa-PanelLayout {
max-height: var(--aa-detached-modal-max-height);
padding-bottom: var(--aa-spacing-half);
position: static;
}
}
}
// Search Button
.aa-DetachedSearchButton {
@apply rounded-md bg-scaleA-200 border border-scale-700;
align-items: center;
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
cursor: pointer;
display: flex;
font: inherit;
font-family: var(--aa-font-family);
font-size: var(--aa-font-size);
height: var(--aa-search-input-height);
margin: 0;
padding: 0 calc(var(--aa-search-input-height) / 8);
position: relative;
text-align: left;
width: 100%;
&:focus {
@apply border-brand-900;
outline: currentColor none medium;
}
@at-root .aa-DetachedSearchButtonIcon {
@apply text-scale-1100;
align-items: center;
cursor: initial;
display: flex;
height: 100%;
justify-content: center;
width: calc(var(--aa-icon-size) + var(--aa-spacing));
}
.aa-DetachedSearchButtonPlaceholder {
@apply text-sm text-scale-800;
}
}
// Remove scroll on `body`
.aa-Detached {
height: 100vh;
overflow: hidden;
}
.aa-DetachedOverlay {
@apply backdrop-blur-sm;
background-color: rgba(0, 0, 0, 0.4);
height: 100vh;
left: 0;
margin: 0;
padding: 0;
position: fixed;
right: 0;
top: 0;
z-index: calc(var(--aa-base-z-index) - 1);
}
//----------------
// 10. Gradients
//----------------
.aa-GradientTop,
.aa-GradientBottom {
height: var(--aa-spacing-half);
left: 0;
pointer-events: none;
position: absolute;
right: 0;
z-index: var(--aa-base-z-index);
}
.aa-GradientTop {
// background-image: linear-gradient(
// rgba(var(--aa-background-color-rgb), 1),
// rgba(var(--aa-background-color-rgb), 0)
// );
top: 0;
}
.aa-GradientBottom {
// background-image: linear-gradient(
// rgba(var(--aa-background-color-rgb), 0),
// rgba(var(--aa-background-color-rgb), 1)
// );
// border-bottom-left-radius: calc(var(--aa-spacing) / 4);
// border-bottom-right-radius: calc(var(--aa-spacing) / 4);
bottom: 0;
}
//----------------
// 11. Utilities
//----------------
.aa-DesktopOnly {
@media (hover: none) and (pointer: coarse) {
display: none;
}
}
.aa-TouchOnly {
@media (hover: hover) {
display: none;
}
}
// search bar
input[type='search'].DocSearch-Input {
background-color: var(--colors-scale1);
box-shadow: none;
}
.dark .DocSearch-Input {
background-color: var(--colors-scale2);
color: white;
}
-535
View File
@@ -1,535 +0,0 @@
/* search modal */
.DocSearch--active {
@apply overflow-hidden #{!important};
}
.DocSearch-Container {
background: rgba(0, 0, 0, 0.3);
@apply fixed top-0 left-0 z-[200] flex h-screen w-screen flex-col p-4 backdrop-blur-sm sm:p-6 md:p-[10vh] lg:p-[12vh];
@apply cursor-auto #{!important};
}
.DocSearch-LoadingIndicator svg {
@apply hidden;
}
.DocSearch-LoadingIndicator {
display: none;
width: 1.5rem;
height: 1.5rem;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'%3E%3Ccircle cx='12' cy='12' r='9' stroke-width='2' stroke='%23cffafe' /%3E%3Cpath d='M3,12a9,9 0 1,0 18,0a9,9 0 1,0 -18,0' stroke-width='2' stroke='%2306b6d4' stroke-dasharray='56.5486677646' stroke-dashoffset='37.6991118431' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-size: 100% 100%;
}
.DocSearch-Container--Stalled .DocSearch-LoadingIndicator {
@apply block;
}
.DocSearch-Modal {
@apply dark:bg-scale-200 bg-scale-100;
margin: 0 auto;
width: 100%;
max-width: 47.375rem;
display: flex;
flex-direction: column;
min-height: 0;
border-radius: theme('borderRadius.lg');
box-shadow: theme('boxShadow.lg');
}
.DocSearch-SearchBar {
z-index: 1;
padding: 0 1rem;
@apply border-scale-400 relative flex flex-none items-center border-b;
}
.DocSearch-Form {
@apply flex min-w-0 flex-auto items-center;
}
.DocSearch-Dropdown-Container {
@apply pb-6;
}
.DocSearch-Dropdown {
@apply flex-auto overflow-auto;
}
.DocSearch-Hit--Result {
@apply relative mx-6;
}
.DocSearch-Hit--Result.DocSearch-Hit--Child {
@apply ml-12;
}
.DocSearch-Hit--Result.DocSearch-Hit--Child::before {
content: '';
position: absolute;
top: -0.25rem;
bottom: -0.25rem;
left: -1rem;
width: 1px;
background: theme('colors.scale.400');
// @apply dark:bg-slate-700;
}
.DocSearch-Hit > a {
position: relative;
font-size: 0.875rem;
// border-bottom: 1px solid theme('colors.slate.100');
// @apply dark:border-slate-200;
}
.DocSearch-Hit--Result {
// @apply border-b-0 #{!important};
}
.DocSearch-MagnifierLabel {
@apply h-6 w-6 flex-none;
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m19 19-3.5-3.5' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='11' cy='11' r='6' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m19 19-3.5-3.5' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='11' cy='11' r='6' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-MagnifierLabel svg {
display: none;
}
.DocSearch-Container--Stalled .DocSearch-MagnifierLabel {
display: none;
}
.DocSearch-Input {
@apply text-scale-1200 placeholder-scale-900;
appearance: none;
background: transparent;
height: 3.5rem;
font-size: 1rem;
margin-left: 0.75rem;
margin-right: 1rem;
flex: auto;
min-width: 0;
border: none;
}
@screen sm {
.DocSearch-Input {
font-size: 0.875rem;
}
}
.DocSearch-Input:focus {
outline: 2px dotted transparent;
}
.DocSearch-Input::-webkit-search-cancel-button,
.DocSearch-Input::-webkit-search-decoration,
.DocSearch-Input::-webkit-search-results-button,
.DocSearch-Input::-webkit-search-results-decoration {
display: none;
}
.DocSearch-Reset {
display: none;
}
button[type='reset'].DocSearch-Cancel {
appearance: none;
flex: none;
font-size: 0;
border-radius: 0.375rem;
padding: 0.25rem 0.375rem;
@apply ring-scale-900 hover:ring-scale-900 dark:bg-scale-600 shadow-sm ring-1 hover:shadow dark:ring-0;
width: 1.75rem;
height: 1.5rem;
/* esc */
background-image: url("data:image/svg+xml,%3Csvg width='16' height='7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M.506 6h3.931V4.986H1.736v-1.39h2.488V2.583H1.736V1.196h2.69V.182H.506V6ZM8.56 1.855h1.18C9.721.818 8.87.102 7.574.102c-1.276 0-2.21.705-2.205 1.762-.003.858.602 1.35 1.585 1.585l.634.159c.633.153.986.335.988.727-.002.426-.406.716-1.03.716-.64 0-1.1-.295-1.14-.878h-1.19c.03 1.259.931 1.91 2.343 1.91 1.42 0 2.256-.68 2.259-1.745-.003-.969-.733-1.483-1.744-1.71l-.523-.125c-.506-.117-.93-.304-.92-.722 0-.375.332-.65.934-.65.588 0 .949.267.994.724ZM15.78 2.219C15.618.875 14.6.102 13.254.102c-1.537 0-2.71 1.086-2.71 2.989 0 1.898 1.153 2.989 2.71 2.989 1.492 0 2.392-.992 2.526-2.063l-1.244-.006c-.117.623-.606.98-1.262.98-.883 0-1.483-.656-1.483-1.9 0-1.21.591-1.9 1.492-1.9.673 0 1.159.389 1.253 1.028h1.244Z' fill='%23334155'/%3E%3C/svg%3E");
background-position: center;
background-repeat: no-repeat;
background-size: 57.1428571429% auto;
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='16' height='7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M.506 6h3.931V4.986H1.736v-1.39h2.488V2.583H1.736V1.196h2.69V.182H.506V6ZM8.56 1.855h1.18C9.721.818 8.87.102 7.574.102c-1.276 0-2.21.705-2.205 1.762-.003.858.602 1.35 1.585 1.585l.634.159c.633.153.986.335.988.727-.002.426-.406.716-1.03.716-.64 0-1.1-.295-1.14-.878h-1.19c.03 1.259.931 1.91 2.343 1.91 1.42 0 2.256-.68 2.259-1.745-.003-.969-.733-1.483-1.744-1.71l-.523-.125c-.506-.117-.93-.304-.92-.722 0-.375.332-.65.934-.65.588 0 .949.267.994.724ZM15.78 2.219C15.618.875 14.6.102 13.254.102c-1.537 0-2.71 1.086-2.71 2.989 0 1.898 1.153 2.989 2.71 2.989 1.492 0 2.392-.992 2.526-2.063l-1.244-.006c-.117.623-.606.98-1.262.98-.883 0-1.483-.656-1.483-1.9 0-1.21.591-1.9 1.492-1.9.673 0 1.159.389 1.253 1.028h1.244Z' fill='%2394a3b8'/%3E%3C/svg%3E");
}
}
.DocSearch-Reset svg {
display: none;
}
.DocSearch-Hit-source {
line-height: 1.5rem;
font-weight: 400;
color: theme('colors.scale.900');
padding-top: 2.5rem;
margin: 0 1.5rem 1rem;
}
.DocSearch-Hits:first-child .DocSearch-Hit-source {
padding-top: 1.5rem;
}
.DocSearch-Hit-Container {
display: flex;
align-items: center;
}
.DocSearch-Hit-Tree {
display: none;
}
.DocSearch-Hit-icon {
display: none;
}
.DocSearch-Hit--Result .DocSearch-Hit-icon {
display: block;
flex: none;
margin-right: 1rem;
width: 1.5rem;
height: 1.5rem;
border-radius: theme('borderRadius.md');
/* hash icon */
background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.75 1v10M8.25 1v10M1 3.75h10M1 8.25h10' stroke='%2394A3B8' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center center;
@apply ring-scale-300 bg-white shadow-sm ring-1 dark:bg-slate-700 dark:ring-0;
}
.DocSearch-Hit--Parent.DocSearch-Hit--Guide .DocSearch-Hit-icon {
/* book icon */
background-image: url("data:image/svg+xml,%3Csvg width='14' height='10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M3.5 0c1.093 0 2.117.27 3 .743V10a6.344 6.344 0 0 0-3-.743c-1.093 0-2.617.27-3.5.743V.743C.883.27 2.407 0 3.5 0Z' fill='%23BAE6FD'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.5 0c1.093 0 2.617.27 3.5.743V10c-.883-.473-2.407-.743-3.5-.743s-2.117.27-3 .743V.743a6.344 6.344 0 0 1 3-.743Z' fill='%2338BDF8'/%3E%3C/svg%3E");
}
.DocSearch-Hit-icon svg {
display: none;
}
.DocSearch-Hit-content-wrapper {
flex: auto;
display: flex;
flex-direction: column-reverse;
min-width: 0;
z-index: 1;
}
.DocSearch-Hit-path {
align-self: flex-start;
font-size: 0.75rem;
line-height: 1.5rem;
font-weight: 400;
color: theme('colors.scale.1100');
border-radius: 999px;
padding: 0 0.75rem;
@apply bg-scale-300 dark:bg-scale-500;
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-icon {
// @apply bg-transparent shadow-none ring-1 ring-white/30 dark:ring-white/20;
// background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.75 1v10M8.25 1v10M1 3.75h10M1 8.25h10' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit--Parent .DocSearch-Hit-icon {
/* book icon */
// background-image: url("data:image/svg+xml,%3Csvg width='14' height='10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M3.5 0c1.093 0 2.117.27 3 .743V10a6.344 6.344 0 0 0-3-.743c-1.093 0-2.617.27-3.5.743V.743C.883.27 2.407 0 3.5 0Z' fill='%23fff'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.5 0c1.093 0 2.617.27 3.5.743V10c-.883-.473-2.407-.743-3.5-.743s-2.117.27-3 .743V.743a6.344 6.344 0 0 1 3-.743Z' fill='%23fff'/%3E%3C/svg%3E");
}
.DocSearch-Hit-title {
@apply text-scale-1100 dark:text-scale-1100 truncate leading-6;
}
.DocSearch-Hit-title + .DocSearch-Hit-path {
@apply mb-1;
}
.DocSearch-Hit-action {
@apply ml-3.5 flex-none;
}
.DocSearch-Hit--Result {
.DocSearch-Hit-action svg {
@apply hidden;
}
.DocSearch-Hit-action {
// @apply h-6 w-6;
// background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m11 9 3 3-3 3' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
[aria-selected='true'] & .DocSearch-Hit-action {
// background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m11 9 3 3-3 3' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.DocSearch-Hit[aria-selected='true'] & .DocSearch-Hit-path {
// @apply bg-scale-400 dark:bg-scale-400;
}
.DocSearch-Hit[aria-selected='true'] & .DocSearch-Hit-title {
// @apply text-white;
}
}
.DocSearch-Hit-action-button {
@apply flex;
}
.DocSearch-Hit-action + .DocSearch-Hit-action {
@apply ml-2;
}
.DocSearch-Hit-action path {
stroke-width: 2px;
stroke: #71717a;
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-action path {
stroke: #777;
.dark & {
stroke: #aaa;
}
}
.DocSearch-Hit > a {
@apply block py-4 px-6;
}
.DocSearch-Hit--Result {
@apply py-3 px-4 #{!important};
}
.DocSearch-Hit {
@apply relative;
}
.DocSearch-Hit:first-child > a {
// @apply border-t border-slate-100 dark:border-slate-200;
}
.DocSearch-Hit--Result {
// @apply border-t-0 #{!important};
}
.DocSearch-Hit + .DocSearch-Hit .DocSearch-Hit--Result {
@apply mt-2;
}
.DocSearch-Hit--Result {
@apply dark:bg-scale-300 border-scale-400 rounded-lg border bg-white transition-all;
}
.DocSearch-Hit[aria-selected='true'] > a {
@apply bg-slate-50 dark:bg-slate-700;
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit--Result {
@apply bg-scale-200 dark:bg-scale-600;
}
.DocSearch-Hit--FirstChild::before {
@apply top-0 #{!important};
}
.DocSearch-Hit--LastChild::before {
@apply bottom-0 #{!important};
}
.DocSearch-Hits mark {
background: none;
@apply text-scale-1200 border-scale-900 border-b-2 font-semibold;
}
.DocSearch-Hit-path mark {
@apply border-0;
}
.DocSearch-Hit[aria-selected='true'] mark {
@apply text-scale-1200;
border-bottom-color: currentColor;
}
.DocSearch-Footer {
@apply flex flex-none justify-end border-t border-slate-100 py-4 px-6 dark:border-slate-200;
}
.DocSearch-Commands {
@apply hidden;
}
.DocSearch-Logo a {
@apply text-scale-900 flex items-center text-xs font-medium;
}
.DocSearch-Logo svg {
@apply text-scale-900 ml-3 text-[#5468ff];
}
.DocSearch-Hit--deleting,
.DocSearch-Hit--favoriting {
opacity: 0;
transition: all 250ms linear;
}
.DocSearch-NoResults .DocSearch-Screen-Icon {
@apply hidden;
}
.DocSearch-Title {
@apply mb-10 text-lg leading-6;
}
.DocSearch-Title strong {
@apply text-slate-1200 dark:text-scale-1200 font-normal;
}
.DocSearch-NoResults-Prefill-List {
.DocSearch-Help {
color: var(--colors-scale11) !important;
}
}
.DocSearch-StartScreen {
.DocSearch-Help {
@apply text-center py-16;
}
}
.DocSearch-Help {
@apply text-scale-1100;
font-size: 0.875rem !important;
a {
color: var(--colors-brand9) !important;
font-weight: 400 !important;
}
}
.DocSearch-NoResults {
@apply px-4 pt-10 pb-8;
}
.DocSearch-NoResults .DocSearch-Title {
text-align: center;
color: theme('colors.scale.1100');
}
.DocSearch-NoResults-Prefill-List .DocSearch-Help {
@apply text-scale-900 dark:text-scale-200 mb-3 text-sm leading-6;
}
.DocSearch-NoResults-Prefill-List ul {
@apply border-scale-300 dark:border-scale-400 dark:bg-scale-300 overflow-hidden rounded-lg border bg-white;
}
.DocSearch-NoResults-Prefill-List button {
padding: 0.5rem 0.75rem;
display: block;
width: 100%;
text-align: left;
font-size: theme('fontSize.sm');
line-height: theme('lineHeight.6');
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m11 9 3 3-3 3' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
@apply text-scale-900 hover:text-slate-1200 dark:bg-scale-300 hover:bg-scale-200 dark:hover:bg-scale-400 bg-white;
}
.DocSearch-NoResults-Prefill-List li + li button {
@apply border-scale-300 border-t dark:border-slate-400;
}
.DocSearch-NoResults-Prefill-List + .DocSearch-Help {
@apply mt-3 text-xs leading-6;
}
.DocSearch-NoResults-Prefill-List + .DocSearch-Help a {
@apply text-scale-900 dark:text-scale-900 font-semibold;
}
.DocSearch-Hit-action [data-cy] {
width: 1.5rem;
height: 1.5rem;
}
.DocSearch-Hit-action [data-cy] svg {
display: none;
}
.DocSearch-Hit-action [data-cy='fav-recent'] {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' stroke='%23CBD5E1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-Hit-action [data-cy='fav-recent']:hover {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' stroke='%230ea5e9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.DocSearch-Hit-action [data-cy='remove-recent'] {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-Hit-action [data-cy='remove-recent']:hover {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-Hit-action [data-cy='remove-fav']::before {
@apply pointer-events-none absolute inset-0 bg-sky-50 content-[''] dark:bg-slate-700;
}
.DocSearch-Hit[aria-selected='true'] [data-cy='remove-fav']::before {
@apply bg-sky-100 dark:bg-slate-700;
}
.DocSearch-Hit-action [data-cy='remove-fav']::after {
content: '';
position: absolute;
pointer-events: none;
width: calc(4.5rem + 1px);
height: 1.5rem;
margin-left: calc(-3rem - 1px);
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat, repeat-y, no-repeat;
background-position: left, center, right;
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' opacity='0.05' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-Hit-action [data-cy='remove-fav']:hover::after {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' opacity='0.05' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-Hit-action + .DocSearch-Hit-action {
// @apply ml-3 border-l border-slate-200 pl-3 dark:border-slate-200;
}
-1
View File
@@ -1 +0,0 @@
module.exports = require('config/eslint-preset')
-30
View File
@@ -1,30 +0,0 @@
## Getting Started
First, run the development server:
```bash
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
-5
View File
@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
-5
View File
@@ -1,5 +0,0 @@
const withTM = require('next-transpile-modules')(['common'])
module.exports = withTM({
reactStrictMode: true,
})
-30
View File
@@ -1,30 +0,0 @@
{
"name": "forum",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev --port 3002",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@octokit/graphql": "^4.8.0",
"colorette": "^1.4.0",
"common": "*",
"next": "12.0.8",
"react": "17.0.2",
"react-dom": "17.0.2",
"string-hash": "^1.1.3"
},
"devDependencies": {
"@types/node": "^17.0.12",
"@types/react": "17.0.37",
"config": "*",
"eslint": "7.32.0",
"mini-svg-data-uri": "^1.4.4",
"next-transpile-modules": "9.0.0",
"tsconfig": "*",
"typescript": "^4.5.3"
}
}
@@ -1 +0,0 @@
module.exports = require('config/postcss.config')
@@ -1,9 +0,0 @@
import React from 'react'
import Main from '.'
import '../styles/tailwind.css'
const Index = () => {
return <Main />
}
export default Index
@@ -1,35 +0,0 @@
import { fetchDiscussions } from '../utils/api'
import { Button, Layouts } from 'common'
import { useEffect, useState } from 'react'
export default function Main() {
const [discussionList, setDiscussionList] = useState([] as any[])
useEffect(() => {
async function fetchData() {
const discussions = await fetchDiscussions()
setDiscussionList(discussions)
}
fetchData()
}, [])
return (
<Layouts.SidebarLayout
sidebarContent={
<div className="p-4 text-white">
<div className="text-sm uppercase">Categories</div>
<div>One</div>
<div>Two</div>
</div>
}
>
<h1 className="text-2xl font-semibold text-gray-900">Discussions</h1>
{discussionList.map((x) => (
<div className="pb-4">
<h2 className="text-lg font-semibold">{x.title}</h2>
<p>{x.body}</p>
</div>
))}
</Layouts.SidebarLayout>
)
}
@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -1,45 +0,0 @@
import { graphql } from '@octokit/graphql'
const query = `
{
repository(owner: "supabase", name: "supabase") {
discussions(first: 10) {
nodes {
id
author {
avatarUrl
url
login
}
body
title
answer {
author {
avatarUrl
login
}
body
createdAt
}
createdAt
}
totalCount
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
}
`
export async function fetchDiscussions() {
// @ts-ignore
const { repository } = await graphql(query, {
headers: {
authorization: `token ${process.env.NEXT_PUBLIC_DISCUSSIONS_TOKEN}`,
},
})
return repository.discussions.nodes
}
@@ -1 +0,0 @@
module.exports = require('config/tailwind.config')
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
@@ -1 +0,0 @@
module.exports = require('config/eslint-preset')
-5
View File
@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
@@ -1,5 +0,0 @@
const withTM = require('next-transpile-modules')(['common'])
module.exports = withTM({
reactStrictMode: true,
})
@@ -1,27 +0,0 @@
{
"name": "tutorials",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev --port 3003",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"colorette": "^1.4.0",
"common": "*",
"next": "12.0.8",
"react": "17.0.2",
"react-dom": "17.0.2"
},
"devDependencies": {
"@types/node": "^17.0.12",
"@types/react": "17.0.37",
"config": "*",
"eslint": "7.32.0",
"next-transpile-modules": "9.0.0",
"tsconfig": "*",
"typescript": "^4.5.3"
}
}
@@ -1 +0,0 @@
module.exports = require('config/postcss.config')
@@ -1,9 +0,0 @@
import React from 'react'
import Main from '.'
import '../styles/tailwind.css'
const Index = () => {
return <Main />
}
export default Index
@@ -1,27 +0,0 @@
import { fetchPosts, DevPost } from '../utils/api'
import { Button, Layouts } from 'common'
import { useEffect, useState } from 'react'
export default function Main() {
const [postList, setPostList] = useState([] as DevPost[])
useEffect(() => {
async function fetchData() {
const posts = await fetchPosts()
setPostList(posts)
}
fetchData()
}, [])
return (
<Layouts.SidebarLayout sidebarContent={<div>Shared Sidebar</div>}>
<h1 className="text-2xl font-semibold text-gray-900">Tutorials</h1>
{postList.map((x) => (
<div className="pb-4">
<h2>{x.title}</h2>
<p>{x.description}</p>
</div>
))}
</Layouts.SidebarLayout>
)
}
@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -1,55 +0,0 @@
const username = 'supabase'
const postsUrl = `https://dev.to/api/articles?username=${username}`
// Defines a Post returned from the DEV API
export type DevPost = {
id: Number
title: string
description: string
slug: string
url: string
path: string
readable_publish_date: string
published_at: string
tags: string
tag_list: string[]
comments_count: Number
public_reactions_count: Number
positive_reactions_count: Number
published: boolean
first_published_at: string
edited_at: string
user: {
username: string
name: string
profile_image_90: string
cover_image_90: string
profile_image_original: string
cover_image_original: string
location: string
website: string
bio: string
twitter_username: string
github_username: string
dribbble_username: string
path: string
}
organization: {
id: Number
name: string
slug: string
profile_image_90: string
profile_imag: string
}
}
export async function fetchPosts() {
try {
const res = await fetch(postsUrl)
const data: DevPost[] = await res.json()
return data
} catch (err) {
console.log(err)
return []
}
}
@@ -1 +0,0 @@
module.exports = require('config/tailwind.config')
@@ -1,5 +0,0 @@
{
"extends": "tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
+38
View File
@@ -0,0 +1,38 @@
---
title: SupaClub
description: The Worlds First Software Engineering Nightclub.
author: ant_wilson
image: supaclub/supaclub-thumb.png
thumb: supaclub/supaclub-thumb.png
tags:
- announcements
date: '2023-04-01'
toc_depth: 3
---
Announcing SupaClub (pronounced “Super-Clurb”), the world’s first Software Engineering Nightclub.
Located on the Indonesian island of Bali, SupaClub is where the digital nomads of South East Asia are mixing Hardcore Techno with Hardcore Engineering.
![Main Dance Floor](/images/blog/supaclub/supaclub1.png)
In March this year, some members of the Supabase team took advantage of the fact that they can work fully remotely, and spent a week working in Bali. The island is a great place to relax in coffee shops during the day, but when night time rolled around our team faced a major problem - the large established nightclubs of Bali would not allow our engineers to bring their laptops into the club. They have strict rules against EMACS on the dance floor, killing our dream of fixing bugs whilst jamming to 90s club anthems.
Our team got together and decided that we would do what we do best at Supabase. We’d ship a fix, and today that fix becomes a reality.
![coding in the club](/images/blog/supaclub/supaclub2.png)
SupaClub, situated near the beach in the Canggu neighborhood of Bali, will open its doors this coming Friday. It can house 4000 developers across 4 floors, each with a dedicated DJ and breakout rooms. It has UltraSpeed 1Gbps WiFi and bookable standing desks on every floor. VIP [SupaBrew bottle service](https://supabase.com/blog/supabrew) is available inside private conference rooms, complete with whiteboards and UltraWide monitors for full immersion (Think WeWork-meets-Pacha).
![dancefloor meets coding](/images/blog/supaclub/supaclub3.png)
In terms of beverage selection, [SupaBrew](https://supabase.com/blog/supabrew) will of course be available behind the bar, along with a cocktail list that includes:
- **Bjarne Pisco Stroustrup** - A back-to-basics tantalizing mix of tart, sweet, and silky pisco brandy that won't leave your pointers dangling
- **The Linus** - A spicy twist on the Bloody Mary, this aggressive blend will have you shouting for more
- **Mojito-js** - we would list the ingredients but our mixologists keep changing the spec
- **HTML Highball** - Not a real cocktail, more of a mocktail
SupaClub was made possible with the help of infamous Fyre Fest investor Ja Rule and “Super Bass” songstress Niki Minaj, both of which will also be performing on the opening night.
If you would like to reserve a spot on the guest list for the opening night please email us at noreply@supabase.com with a link to your GitHub account and "Re: april fools" in the subject line. See you there.
+97
View File
@@ -0,0 +1,97 @@
---
title: Epsilon3 digitize paper-based procedures in the space industry using telemetry data, simplifying testing and operations.
name: Epsilon3
description: Epsilon3 uses Supabase to help teams execute secure and reliable operations in an industry where project spend runs into the billions.
author: rory_wilding
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
authorURL: https://github.com/kiwicopple
logo: /images/customers/logos/epsilon3.png
logo_inverse: /images/customers/logos/light/epsilon3.png
og_image: /images/customers/og/epsilon3.jpg
tags:
- supabase
date: '2023-02-14'
company_url: 'https://epsilon3.io'
stats:
[
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
]
misc: [{ label: 'Founded', text: Los Angeles, California }]
about: Epsilon3 is the OS for spacecraft and complex operations. Epsilon3’s software platform manages complex operational procedures, saving operators time and reducing errors.
---
Epsilon3 is a USA-based Y Combinator startup. They digitize paper-based procedures in the space industry using telemetry data,
simplifying testing and operations. Their product is rapidly becoming the OS for space projects and complex operations.
## Space Operations
Epsilon3's customers run testing and operational procedures in real-time. Epsilon3's solution allows online collaboration as a team,
instead of being siloed and separated which is an increasingly important requirement in a post-pandemic working world.
Epsilon3 is powering an industry where reliability is paramount. In the space industry, missions cost billions and need to run reliably.
![Epsilon3 digitizes paper-based processes.](/images/blog/epsilon3/epsilon3-product.gif)
Epsilon3's software saves operators time and reduces errors to improve complex operations. Their customers are using their software and
successfully sending satellites into space and are about to use Epsilon3's software for an operational Rocket Launch. As a result, their
reputation for improving the quality standard for complex operations has led to demand from other industries. The team is receiving
enquiries from clients in the robotics, logistics and medical equipment sectors who need a structured and standardized way of running
procedures and keeping audit trails.
## Moving fast in regulated markets
As a startup, Epsilon3 needs to move fast and scale. They require security, reliability, and performance.
The team has a background in engineering from Northrop, Google, and SpaceX and understands the technical benefits Postgres offers as a
battle-tested open-source relational database.
Real-time functionality is also a key for their customers to see how running procedures are progressing as the changes occur.
The space industry is highly regulated, adding complexity. For example, in the USA, deployments must be in an ITAR-compliant way
(for instance AWS GovCloud supports ITAR compliance) while customers in countries outside the USA may need to host their data within their own
country. These customer requirements mean Epsilon3 must be able to offer flexible hosting options, including on-premises deployment.
<Quote
img="aaron-epsilon3.png"
caption="Aaron Sullivan, ex-Google engineer, Stanford University Alumni, Principal Software Engineer Epsilon3."
>
<p>Billion dollar missions need to run reliably and securely.</p>
<p>
We didn’t just want something that works, we wanted a solution implementing best practices. We
also needed a setup that we could self-host to meet compliance needs and allow on-premises
deployment options for our customers.
</p>
<p>
We use Supabase because they give us an open-source scalable back-end built by database experts
that we can self-host.
</p>
<p>
Supabase takes out the mental effort from our back-end infrastructure so we can focus on our
customers needs.
</p>
</Quote>
## Supabase gives Postgres users superpowers
The team implemented a self-hosted Supabase setup. They use AWS with a self-hosted Supabase API powering an RDS Postgres instance.
This setup gives a balance of rapid development, reduced DevOps, and flexible hosting options. As Supabase uniquely offers [real-time](https://supabase.com/realtime) for
Postgres, it allows their customers to see updates from running procedures. Supabase offers well-supported client libraries that make
Postgres Row Level Security easy to use, which is a critical item on the Epsilon3 roadmap.
All this was easy for the team to set up, even with self-hosting, so the team was able to focus on what they do best&mdash;helping their customers
run complex operations smoothly. With this implementation, Epsilon3 gets all the benefits of Postgres, plus the smooth developer experience and
support that Supabase is known for, without compromising their compliance needs.
![Epsilon3 uses Supabase for hosting their critical infrastructure.](/images/blog/epsilon3/supabase-epsilon3-architecture.png)
## Self-hosted Supabase, a superpower
Self-hosting Supabase allows the crew at Epsilon3 to move fast. They are revolutionizing space operations securely and reliably in real-time whilst
staying compliant. The team is now seeing demand from other industries and is confident in their scaling capability because they know it's
just Postgres under the hood.
Check out the [Epsilon3 website](https://www.epsilon3.io/) to learn more about the team and their OS for space operations. If you are interested
in self-hosting Supabase then you can **[contact us](https://forms.supabase.com/enterprise)** to learn more.
+72
View File
@@ -0,0 +1,72 @@
---
title: HappyTeams unlocks better performance and reduces cost with Supabase.
name: HappyTeams
description: How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back.
author: rory_wilding
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
authorURL: https://github.com/kiwicopple
logo: /images/customers/logos/happyteams.png
logo_inverse: /images/customers/logos/light/happyteams.png
og_image: /images/customers/og/happyteams.jpg
tags:
- supabase
date: '2023-02-16'
company_url: 'https://happyteams.io/'
stats:
[
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
]
misc: [{ label: 'Founded', text: 'Dublin, Ireland' }]
about: HappyTeams is a purpose-built solution that empowers management to unlock valuable team insights.
---
## The Challenge
> Finding a painless and hands-free database experience to support growth
[HappyTeams](https://happyteams.io/) is a growing startup with an ambitious vision. The company is bootstrapped, so technical resources are scarce. Devs need to do more with less. This means staying laser-focused on delivering features, rather than database timeouts or performance issues.
The company launched its app on Heroku Postgres. But one year in, they were experiencing scaling issues. They needed to handle thousands of users while also shipping new features and building out integrations. Issues with database slowness and concurrent connections were holding them back.
Michael Dever, the founder and CTO of HappyTeams, realized it was time for a change. A large overhaul of the app in early 2022 presented the opportunity to do so: "When you're in that first sprint of building a product, you're kind of locked with that first database decision you make. When we did a rebuild, it was time to look at something like Supabase."
![HappyTeams Home Page](/images/blog/2023-02-16-case-study-happyteams/happyteams-home-page.png)
## Why Supabase?
There were four main factors that led HappyTeams to switch from Heroku Postgres to Supabase:
1. **Better performance**. The team was experiencing uneven performance with Heroku, which was sluggish at times. Supabase was consistently fast.
2. **Enabling a large number of direct connections and connection pooling with PgBouncer**. HappyTeams was constantly hitting limits on the number of database connections. They could solve this problem with Supabase's [flexible plans](https://supabase.com/pricing) and [support for PgBouncer](https://supabase.com/blog/supabase-pgbouncer).
3. **Quality of support, documentation, and onboarding**: While Heroku provided no support at the tier they were on, the Supabase team was readily available: "Responses were powerful and they answered all my questions, even on a Sunday. That just further sold the product for me." He was also impressed with the [documentation](https://supabase.com/docs) on the Supabase website and the onboarding process.
4. **Lower costs**. Moving to Supabase reduced HappyTeams's costs: The base plan comes with everything they need to scale the product.
<Quote img="michael-dever-happyteams.jpeg" caption="Michael Dever - CTO of HappyTeams">
Great documentation, great product, great pricing. It sells itself, really.
</Quote>
## Supabase provides a fast and reliable production database
After a 30 minute migration, HappyTeams can iterate quickly, add new features, and improve their product based on customer feedback
Once they made the decision, migrating from Heroku to Supabase was simple. HappyTeams completed their database migration in less than an hour using pg_dump and pg_restore.
Performance is no longer an issue. "Supabase just worked", says Michael. Pooling connections using PgBouncer was effortless - solving a recurring pain point for HappyTeams.
"Supabase is so fast. Way, way, way faster than our Heroku Postgres database, which I think was located in the same region."
Today, Supabase is an integral part of HappyTeams's tech stack. It has allowed the company to move fast and rapidly release new features - with minimal tech resources devoted to their backend infrastructure.
Thanks to Supabase's ease of use and reliability, HappyTeams can iterate quickly based on feature requests and deliver more value to their user base. They are planning to expand their usage, exploring functionality such as [Edge Functions](https://supabase.com/edge-functions) and the [API client](https://supabase.com/docs/guides/api).
<Quote img="michael-dever-happyteams.jpeg" caption="Michael Dever - CTO of HappyTeams">
When we needed to add a comments feature to our app, we could get it done in just two days.
Supabase is really like a dream come true for a developer.
</Quote>
+97
View File
@@ -0,0 +1,97 @@
---
title: How Mobbin migrated 200,000 users from Firebase for a better authentication experience.
name: Mobbin
description: Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps.
author: rory_wilding
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
authorURL: https://github.com/kiwicopple
logo: /images/customers/logos/mobbin.png
logo_inverse: /images/customers/logos/light/mobbin.png
og_image: /images/customers/og/mobbin.jpg
tags:
- supabase
date: '2023-02-14'
company_url: 'https://mobbin.design'
stats:
[
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
]
misc: [{ label: 'Founded', text: Singapore }]
about: Mobbin is a comprehensive, expert-curated library of interfaces from the best-in-class digital products that helps product designers find relevant design inspiration and references with significantly less time and effort.
---
Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps.
Their platform allows creators to search screenshots of mobile apps to inspire product development.
Creators use Mobbin to combine inspiration and develop new experiences.
## From Idea to 30,000 users in one Product Hunt launch
The initial concept for Mobbin was just a Dropbox full of screenshots. After sharing with their early users,
the team tried organizing them into folders but this quickly became unscalable. Clear interest from users encouraged them to build a full website. They used Firebase for the backend and launched on
[Product Hunt](https://www.producthunt.com/posts/mobbin-1).
Mobbin went viral, and within the first week, they had 30,000 users. They have now more than 400,000
registered users through word of mouth. Their curation continues to attract designers and creators globally.
![Mobbin cover](/images/blog/mobbin/mobbin-cover.png)
## Scaling issues put Mobbin growth at risk
Firebase helped the team launch quickly, but at scale, they found Firebase lacked the functionality required for a high-quality user experience.
The first problem was duplicate user logins. Firebase Authentication doesn't automatically merge users with the same email address.
For example, users cannot log in with Google and Facebook if they share the same email address.
Users struggled to upgrade to a paid account because they couldn't remember which authentication provider they signed up with.
Next, they had issues with data integrity. While Firebase has 99.95% uptime, application code can (and does) fail.
Mobbin didn't want to implement idempotence for every mutative Firebase Function. Between a failed Function execution and a retry,
a document state might change without strict validation. The team could never be confident their data had the level of integrity they required.
Finally, the team grew concerned with their increasing Firebase bill. Because of the document-based data structure,
their API requests required several round trips. At one point, they were making 5 million API requests per month.
These round trips made their website slow and led to a notable time investment hacking together a fix.
![Mobbing screenshots](/images/blog/mobbin/mobbin-screenshots.jpg)
## Migrating to Supabase
Mobbin's initial product gave them a comprehensive understanding of customer requirements. For their second version,
they needed a better authentication experience, fairer pricing, and a relational database to deliver a quality
application sustainable for their business model.
The team found Supabase and realized it covered all the bases. Not only that, their team had experience with the
tools open source tools that Supabase is built with.
[Authentication](https://supabase.com/auth), built using [GoTrue](https://github.com/supabase/gotrue), instantly solved their users' login issues.
Under the hood, Supabase is just Postgres, and Jian Jie (co-founder and CTO of Mobbin) knows this is one of the best battle-tested
open source relational databases.
They decided that moving to Supabase was a no-brainer. The process itself was smooth - the real challenge was validating their
data integrity due to historical failures on their serverless functions.
Mobbin is now successfully running on top of Supabase, with superior performance compared to their old Firebase setup.
Because Supabase does not charge based on API requests, Mobbin significantly reduced their monthly spending without maintaining a custom backend.
<Quote img="jian-mobbin.jpg" caption="Jian Jie Liau, Co-founder and CTO at Mobbin.">
<p>Migrating to Supabase meant that we could instantly fix our Auth problems and save money.</p>
<p>
Just being on Supabase alone gives us confidence we can deliver on whatever users need in the
future.
</p>
<p>
The cool thing is now we know under the hood it's just Postgres, it really does feel like
anything is possible for the future of our product
</p>
</Quote>
## Supabase saves Mobbin time and money
Migrating to Supabase helped the team at Mobbin instantly improve the end-user experience and save costs.
They now add new features with confidence, and they continue to help creators all over the globe find inspiration for their next project.
You can [check out Mobbin on their website](https://mobbin.design/browse/ios/apps).
[Sign up](https://app.supabase.com/) for Supabase's free tier and set up a scalable backend in less than 2 minutes.
+87
View File
@@ -0,0 +1,87 @@
---
title: Replenysh uses Supabase to implement OTP in less than 24 hours.
name: Replenysh
description: With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres.
author: rory_wilding
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
authorURL: https://github.com/kiwicopple
logo: /images/customers/logos/replenysh.png
logo_inverse: /images/customers/logos/light/replenysh.png
og_image: /images/customers/og/replenysh.jpg
tags:
- supabase
date: '2023-02-14'
company_url: https://replenysh.com/
stats:
[
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
]
misc: [{ label: 'Founded', text: Southern California }]
about: Replenysh is a digital platform that connects global brands with local communities to recover materials and eliminate the concept of waste.
---
Replenysh connects brands who want to recover their products and packaging with communities looking to monetize used materials.
Learn how Replenysh uses Supabase to power the circular economy, redefining how brands interact with their customers & products.
## Material recovery offers brands a new way to connect to their customers
At the end of a linear economy, products usually end up in landfills or the ocean. Every year, 8 million metric tons of plastics enter our ocean. This is on top of the estimated 150 million metric tons that currently circulate our marine environments. In the USA alone, every year, 139 million tons of waste is sent to landfills.
The problem is our current waste and recycling infrastructure can't support that demand. With less than 10% of material actually getting recycled, the vast majority just ends up in a landfill or as pollution.
Replenysh is building a solution to this problem by creating entirely new infrastructure to help brands get their materials back. They're building an ecosystem of tools to enable any community in the world to do the right thing.
Whenever material is dropped off at a host, brands have the opportunity to engage with that person, resulting in a win-win situation for the planet, the brands, and the individual who all want to make a positive impact. Anyone in the community can take their used products and materials to the hosts — with that material being tracked all the way back to the brands and manufacturers who will reuse it. Brands have the opportunity to engage with that person, resulting in a win-win situation for the planet, the brands, and the individual who all want to make a positive impact. This turns a traditional consumption model into an opportunity for brands and customers to participate in the circular economy.
Brands are adopting Replenysh to demonstrate their commitment to making a positive environmental impact, while seeking to engage with consumers in a new, authentic way. Replenysh provides the technology to communities that are helping build the future infrastructure where all materials are traceable and reused.
![Linear vs Circular economy](/images/blog/replenysh/circular-economy.png)
## Refactoring code leads to a better way forward
Historically the team created their own backend infrastructure, setting up APIs and Authentication to run their own OTP system with Twilio and passwords. Postgres is essential to their requirements because Replenysh use Row Level Security for authorization. Before adopting Supabase, the team used a Haskell server with a different Postgres instance deployed on Heroku.
The team decided to do a large refactor of their codebase and began investigating alternative hosting options to help speed up development time and reduce maintenance.
Firebase seemed appealing due to its ability to get started quickly, but the team didn't want to migrate away from a relational database. Specifically, Postgres and RLS fit their requirements.
When the team discovered Supabase on Hacker News they felt excitement. Supabase has a reputation for a smooth developer experience, providing Postgres and Row Level Security as a core feature.
As a bonus, Supabase provided more than just a database, reducing their DevOps overhead. After reading the [Supabase Auth documentation](https://supabase.com/docs/guides/auth/overview) the team determined they can have the functionality they need, like SMS OTP. When they saw [Mobbin are using Supabase at scale](https://supabase.com/case-studies-mobbin), they decided to migrate to Supabase.
## Globally recognized brands need quality at scale
Less than a day after beginning development with Supabase, the team completed their SMS OTP implementation, and deployed their app to the app store a few weeks later.
![Replenysh-Mobile-App](/images/blog/replenysh/Replenysh-Mobile-App.png)
Replenysh clients include brands with global recognition and millions of customers. With their reputation at stake, major brands have a high-quality threshold. If a global brand asks its customers to adopt a technology, the user experience must be high quality and reliable. Their customers' primary interaction point is mobile-first. This means a robust and smooth mobile Auth experience is non-negotiable. After switching to Supabase, when they demo, clients are instantly impressed with just how easy it is for users to log in with an SMS OTP and start participating in the circular economy. As a result, some of the world's largest retailers and beverage companies have recently joined Replenysh and the circular economy.
![Replenysh-large-screen-view](/images/blog/replenysh/Replenysh-large-screen-view.png)
## Supabase means faster development and reduced maintenance
<Quote img="clark-dinnison.jpeg" caption="Clark Dinnison, Head of Product at Replenysh.">
<p>
We saw the Auth update for launch week we thought it was the perfect time to get going with
Supabase.
</p>
<p>
We implemented & pushed live phone (OTP) auth and had something ready to deploy to the app store
in less than 24-hours!
</p>
<p>
Supabase's developer experience lived up to its reputation. Our app is ready to launch with
major brands to help them reach sustainability goals and connect with their customers in a
meaningful way
</p>
</Quote>
## Supabase help Replenysh continue to scale
Supabase turbo-charged Replenysh's development time with a seamless end-user mobile login experience. They are ready to help major brands engage with their users through participation in the circular economy. With Supabase, the team knows they have a slick auth experience, reduced DevOps overhead, and can continue to scale with Postgres. You can test out the Supabase developer experience today by [starting a new project on the free tier!.](https://app.supabase.com)
+43
View File
@@ -0,0 +1,43 @@
---
title: Xendit use Supabase and create a full solution shipped to production in less than one week.
name: Xendit
description: As a payment processor, Xendit are responsible for verifying that all transactions are legal.
author: rory_wilding
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
authorURL: https://github.com/kiwicopple
logo: /images/customers/logos/xendit.png
logo_inverse: /images/customers/logos/light/xendit.png
og_image: /images/customers/og/xendit.jpg
company_url: https://xendit.co
tags:
- supabase
date: '2023-02-14'
stats:
[
{ stat: '120', label: Staff count },
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
]
misc: [{ label: 'Founded', text: Jakarta Selatan }]
about: Xendit is a financial technology company that provides payment solutions and simplifies the payment process for businesses in Indonesia, the Philippines, and Southeast Asia, from SMEs and e-commerce startups to large enterprises.
---
## Challenge
As a payment processor, Xendit are responsible for verifying that all transactions are legal. Any transactions which are suspicions must be verified against a strict set of criteria, and the parties involved need to be checked against international sanctions lists. This is a critical anti-money-laundering operation and needs to be performed in realtime to prevent any delays on legitimate payments.
## Why they chose Supabase
Xendit needed something fast. Something that was cheaper than using the global players like Worldcheck or Refinitiv. Xendit already uses Postgres for a lot of their critical infrastructure, and so Xendit team are familiar with the technology and comfortable in it's ability to scale.
## What they built
Xendit parses international sanctions lists from the UN and the Indonesian government and loads them into Supabase. Since Supabase provides a full Postgres server, they can then use the [Trigram](https://www.postgresql.org/docs/current/pgtrgm.html) extension to perform full-text search on the lists, with a relevance score on every search.
Supabase was perfect for their use case, as they needed something built fast. The full solution was built and in production in less than one week.
> The full solution was built and in production in less than one week.
Xendit created a database function for searching, which they are able to call directly using their Python clients. They have plans to iterate on the current implementation using more advanced techniques, like machine learning, but for now the Supabase system has been in Production for 9 months without a problem.
@@ -0,0 +1,54 @@
import SectionHeader from 'components/UI/SectionHeader'
import PostTypes from '~/types/post'
import SectionContainer from '../Layouts/SectionContainer'
import Link from 'next/link'
import { GlassPanel } from 'ui'
import TextLink from '../TextLink'
const CustomerStories = ({ customerStories }: any) => {
const caseStudyThumbs = customerStories.map((blog: PostTypes, idx: number) => {
return {
logo: blog.logo,
logoInverse: blog.logo_inverse,
title: blog.title,
link: blog.url,
}
})
return (
<SectionContainer>
<div className="mb-12">
<SectionHeader
title={'Infrastructure'}
title_alt={' to innovate and scale with ease.'}
subtitle={'Customer Stories'}
paragraph={
'See how Supabase empowers companies of all sizes to accelerate their growth and streamline their work.'
}
/>
</div>
<div className="mx-auto mt-5 grid grid-cols-12 gap-8 lg:gap-12">
{caseStudyThumbs.map((caseStudy: any, i: number) => (
<Link href={`${caseStudy.link}`} key={caseStudy.title} passHref>
<a className="col-span-12 md:col-span-4">
<GlassPanel
{...caseStudy}
background={true}
showIconBg={true}
showLink={true}
hasLightIcon
>
{caseStudy.description}
</GlassPanel>
</a>
</Link>
))}
</div>
<div className="mt-12">
<TextLink url="/customers" label="Explore more" />
</div>
</SectionContainer>
)
}
export default CustomerStories
+20 -5
View File
@@ -1,7 +1,7 @@
import { useRouter } from 'next/router'
import Image from 'next/image'
import Link from 'next/link'
import { motion } from 'framer-motion'
interface iImages {
name: string
image: string
@@ -19,6 +19,7 @@ interface iImageGrid {
lgCols?: colSizes
padding?: paddingSizes
className?: string
animated?: boolean
}
const ImageGrid = ({
@@ -28,6 +29,7 @@ const ImageGrid = ({
lgCols = 8,
padding = 8,
className,
animated = false,
}: iImageGrid) => {
const smBreakpoint = {
3: 'grid-cols-3',
@@ -70,7 +72,7 @@ const ImageGrid = ({
if (x.link) {
return (
<Link href={link}>
<div className="scale-100 transform cursor-pointer duration-100 ease-in-out hover:z-50 hover:scale-105 hover:shadow-sm">
<div className="scale-100 transform cursor-pointer duration-100 ease-in-out hover:shadow-sm">
{children}
</div>
</Link>
@@ -80,9 +82,22 @@ const ImageGrid = ({
}
}
const MaybeAnimatedDiv = ({ children, ...rest }: any) =>
animated ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0, transition: { delay: i / 10 } }}
{...rest}
>
{children}
</motion.div>
) : (
<div {...rest}>{children}</div>
)
return (
<Container link={x.link} key={i}>
<div
<MaybeAnimatedDiv
key={`${x.name}-${i}`}
className={`
dark:bg-scale-400 col-span-1 flex items-center
@@ -106,7 +121,7 @@ const ImageGrid = ({
"
/>
</div>
</div>
</MaybeAnimatedDiv>
</Container>
)
})}
@@ -14,10 +14,10 @@ interface Props {
export default function TicketBrickWall({ users }: Props) {
const isMobile = useMobileViewport(768)
return (
<div className="relative pt-28 pb-20 md:pt-40">
<div className="relative pb-20 pt-28 md:pt-40">
<div className="mx-auto bg-[#1C1C1C]">
<div className="max-w-[38rem] mb-8 mx-auto">
<h2 className="radial-gradient-text-scale-500 text-center">
<h2 className="text-center radial-gradient-text-scale-500">
Explore the unique tickets generated by our community
</h2>
</div>
@@ -34,11 +34,13 @@ export default function TicketBrickWall({ users }: Props) {
/>
{isMobile && <TicketBrickWallSlider users={users.slice(11, 17)} speed={16000} />}
</div>
<div className="w-full mt-2 lg:mt-4 mx-auto flex justify-center">
<div className="flex justify-center w-full mx-auto mt-2 lg:mt-4">
<Link href="/launch-week/tickets">
<Button as="a" type="outline" size="medium">
View all tickets
</Button>
<a>
<Button type="outline" size="medium">
View all tickets
</Button>
</a>
</Link>
</div>
</div>
@@ -48,7 +48,9 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt
.select('*')
.eq('username', username)
.single()
setUserData(data)
if (data) {
setUserData(data)
}
setFormState('default')
// Prefetch GitHub avatar
@@ -66,7 +68,7 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt
setPageState('ticket')
// Listen to realtime changes
if (!realtimeChannel && !data.golden) {
if (!realtimeChannel && !data?.golden) {
const channel = supabase
.channel('changes')
.on(
+6 -6
View File
@@ -62,23 +62,23 @@ const Developers = () => {
<div className="m-3 mx-6">
<p className="p">Latest announcements</p>
<ul className="mt-6 space-y-3 pb-6">
{AnnouncementsData.map((caseStudy: any, idx: number) => (
{AnnouncementsData.map((announcement: any, idx: number) => (
<li className="flow-root" key={`flyout_case_${idx}`}>
<Link href={caseStudy.url}>
<Link href={announcement.url}>
<a className="dark:hover:bg-dark-700 flex items-center rounded-lg border p-3 transition duration-150 ease-in-out hover:bg-gray-100">
<div className="relative hidden h-20 w-32 flex-shrink-0 overflow-auto rounded-md sm:block">
<Image
src={`${basePath}${caseStudy.imgUrl}`}
alt="caseStudyThumb"
src={`${basePath}${announcement.imgUrl}`}
alt={announcement.title}
layout="fill"
objectFit="cover"
/>
</div>
<div className="min-w-0 flex-1 sm:ml-4">
<h4 className="text-scale-1200 text-normal mb-0 text-base">
{caseStudy.title}
{announcement.title}
</h4>
<p className="p text-sm">{caseStudy.description}</p>
<p className="p text-sm">{announcement.description}</p>
</div>
</a>
</Link>
+16 -11
View File
@@ -1,11 +1,12 @@
import React from 'react'
import SolutionsData from 'data/Solutions.json'
import CaseStudiesData from 'data/CaseStudies.json'
import CustomerStoriesData from 'data/CustomerStories.json'
import { useRouter } from 'next/router'
import ProductIcon from '../ProductIcon'
import { Badge } from 'ui'
import Image from 'next/image'
import Link from 'next/link'
import TextLink from '../TextLink'
const Product = () => {
const { basePath } = useRouter()
@@ -46,30 +47,31 @@ const Product = () => {
<div className="m-3 grid grid-cols-12 gap-x-8 gap-y-4 py-4 pr-3">{iconSections}</div>
</nav>
<div className="col-span-6">
<div className="m-3 mx-6">
<p className="p">Latest case studies</p>
<div className="m-3 mx-6 flex flex-col items-start">
<p className="p">Latest customer stories</p>
<ul className="mt-6 space-y-3">
{CaseStudiesData.map((caseStudy: any, idx: number) => {
{CustomerStoriesData.map((customer: any, idx: number) => {
if (idx > 1) {
return null
}
return (
<li className="flow-root" key={`flyout_case_${idx}`}>
<Link href={caseStudy.url}>
<Link href={customer.url}>
<a className="dark:hover:bg-dark-700 flex items-center rounded-lg border p-3 transition duration-150 ease-in-out hover:bg-gray-100">
<div className="relative hidden h-20 w-32 flex-shrink-0 overflow-auto rounded-md sm:block">
<div className="relative hidden mx-2 h-12 w-32 flex-shrink-0 overflow-auto rounded-md sm:block">
<Image
src={`${basePath}/${caseStudy.imgUrl}`}
alt="caseStudyThumb"
src={`${basePath}/${customer.imgUrl}`}
alt={customer.title}
layout="fill"
objectFit="cover"
objectFit="contain"
className="dark:brightness-200 dark:contrast-0 dark:filter"
/>
</div>
<div className="min-w-0 flex-1 sm:ml-4">
<h4 className="text-scale-1200 text-normal mb-0 text-base">
{caseStudy.title}
{customer.title}
</h4>
<p className="p text-sm">{caseStudy.description}</p>
<p className="p text-sm !mb-0">{customer.description}</p>
</div>
</a>
</Link>
@@ -77,6 +79,9 @@ const Product = () => {
)
})}
</ul>
<div className="mt-6">
<TextLink url="/customers" label="Explore more" />
</div>
</div>
</div>
</div>
+29 -31
View File
@@ -273,41 +273,39 @@ const Nav = () => {
</div>
</div>
<div className="flex items-center gap-2">
<Button
as="a"
className="hidden group lg:flex"
// @ts-ignore
href="https://github.com/supabase/supabase"
target="_blank"
type="text"
icon={
<div className="flex items-center justify-center w-4 h-4 text-brand-800">
<div
className={[
`text-scale-900 flex h-3 w-3 items-center justify-center
<a href="https://github.com/supabase/supabase" target="_blank">
<Button
className="hidden group lg:flex"
type="text"
icon={
<div className="flex items-center justify-center w-4 h-4 text-brand-800">
<div
className={[
`text-scale-900 flex h-3 w-3 items-center justify-center
transition-all
group-hover:h-4
group-hover:w-4
group-hover:text-yellow-900
group-focus:h-4
group-focus:w-4
transition-all
group-hover:h-4
group-hover:w-4
group-hover:text-yellow-900
group-focus:h-4
group-focus:w-4
group-focus:text-yellow-900`,
showLaunchWeekNavMode && '!text-white',
].join(' ')}
>
<IconStar strokeWidth={2} />
group-focus:text-yellow-900`,
showLaunchWeekNavMode && '!text-white',
].join(' ')}
>
<IconStar strokeWidth={2} />
</div>
</div>
</div>
}
>
<span
// className={isLaunchWeekPage ? '!text-white' : ''}
}
>
Star us on GitHub
</span>
</Button>
<span
// className={isLaunchWeekPage ? '!text-white' : ''}
>
Star us on GitHub
</span>
</Button>
</a>
{isLoggedIn ? (
<Link href="/dashboard/projects">
+1 -1
View File
@@ -2,7 +2,7 @@
{
"type": "Case Study",
"title": "A web crawler handling millions of API requests.",
"description": "See how Monitoro built an automated a scraping platform using Supabase.",
"description": "See how Monitoro built and automated a scraping platform using Supabase.",
"imgUrl": "images/case-study-monitoro.jpg",
"logoUrl": "images/logos/monitoro.png",
"organization": "Monitoro",
+53
View File
@@ -0,0 +1,53 @@
[
{
"type": "Customer Story",
"title": "HappyTeams unlocks better performance and reduces cost with Supabase.",
"description": "How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back.",
"imgUrl": "images/customers/logos/happyteams.png",
"logoUrl": "images/customers/logos/happyteams.png",
"organization": "HappyTeams",
"url": "/customers/happyteams",
"path": "/customers/happyteams",
"postMeta": {
"name": "Paul Copplestone",
"avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4",
"publishDate": "Feb 16, 2023",
"readLength": 6
},
"ctaText": "View story"
},
{
"type": "Customer Story",
"title": "Xendit use Supabase and create a full solution shipped to production in less than one week.",
"description": "As a payment processor, Xendit are responsible for verifying that all transactions are legal.",
"imgUrl": "images/customers/logos/xendit.png",
"logoUrl": "images/customers/logos/xendit.png",
"organization": "Xendit",
"url": "/customers/xendit",
"path": "/customers/xendit",
"postMeta": {
"name": "Paul Copplestone",
"avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4",
"publishDate": "Feb 14, 2023",
"readLength": 6
},
"ctaText": "View story"
},
{
"type": "Customer Story",
"title": "Replenysh uses Supabase to implement OTP in less than 24 hours.",
"description": "With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres.",
"imgUrl": "images/customers/logos/replenysh.png",
"logoUrl": "images/customers/logos/replenysh.png",
"organization": "Replenysh",
"url": "/customers/replenysh",
"path": "/customers/replenysh",
"postMeta": {
"name": "Paul Copplestone",
"avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4",
"publishDate": "Feb 14, 2023",
"readLength": 6
},
"ctaText": "View story"
}
]
+4
View File
@@ -105,6 +105,10 @@
"text": "Blog",
"url": "/blog"
},
{
"text": "Customer Stories",
"url": "/customers"
},
{
"text": "Careers",
"url": "/careers"
+16 -4
View File
@@ -15,6 +15,8 @@ async function generate() {
'pages/*/*.tsx',
'data/**/*.mdx',
'_blog/*.mdx',
'_case-studies/*.mdx',
'_customers/*.mdx',
'_alternatives/*.mdx',
'!pages/index.tsx',
'!data/*.mdx',
@@ -24,6 +26,10 @@ async function generate() {
'!pages/404.js',
])
const blogUrl = 'blog'
const caseStudiesUrl = 'case-studies'
const customerStoriesUrl = 'customers'
const sitemap = `
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
@@ -33,7 +39,9 @@ async function generate() {
const path = page
.replace('pages', '')
// add a `/` for blog posts
.replace('_blog', '/blog')
.replace('_blog', `/${blogUrl}`)
.replace('_case-studies', `/${caseStudiesUrl}`)
.replace('_customers', `/${customerStoriesUrl}`)
.replace('_alternatives', '/alternatives')
.replace('.tsx', '')
.replace('.mdx', '')
@@ -49,15 +57,19 @@ async function generate() {
if (route === '/alternatives/[slug]') return null
if (route === '/partners/[slug]') return null
if (route === '/case-studies/[slug]') return null
if (route === '/customers/[slug]') return null
if (route === '/launch-week/ticket-image') return null
/**
* Blog based urls
* handle removal of dates in filename
*/
if (route.includes('/blog/')) {
if (route.includes(`/${blogUrl}/`)) {
/**
* remove directory from route
*/
const _route = route.replace('/blog/', '')
const _route = route.replace(`/${blogUrl}/`, '')
/**
* remove the date from the file name
*/
@@ -65,7 +77,7 @@ async function generate() {
/**
* reconsruct the route
*/
route = '/blog/' + substring
route = `/${blogUrl}/` + substring
}
return `
+1 -1
View File
@@ -3,7 +3,7 @@ import path from 'path'
import matter from 'gray-matter'
import { generateReadingTime } from './helpers'
type Directories = '_blog' | '_case-studies' | '_alternatives'
type Directories = '_blog' | '_case-studies' | '_customers' | '_alternatives'
// substring amount for file names
// based on YYYY-MM-DD format
+21 -10
View File
@@ -1827,16 +1827,6 @@ module.exports = [
source: '/legal/soc2',
destination: 'https://forms.supabase.com/soc2',
},
{
permanent: true,
source: '/docs/guides/api',
destination: '/docs/guides/database/api',
},
{
permanent: true,
source: '/docs/guides/api/generating-types',
destination: '/docs/guides/database/api/generating-types',
},
{
permanent: true,
source: '/docs/reference/javascript/upgrade-guide',
@@ -1892,4 +1882,25 @@ module.exports = [
source: '/docs/guides/realtime/postgres-changes',
destination: '/docs/guides/realtime/extensions/postgres-changes',
},
// Serverless APIs
{
permanent: true,
source: '/docs/guides/database/api',
destination: '/docs/guides/api',
},
{
permanent: true,
source: '/docs/guides/database/api/generating-types',
destination: '/docs/guides/api/rest/generating-types',
},
{
permanent: true,
source: '/docs/guides/api/generating-types',
destination: '/docs/guides/api/rest/generating-types',
},
{
permanent: true,
source: '/docs/guides/integrations/dashibase',
destination: '/docs',
},
]
+2 -2
View File
@@ -25,13 +25,13 @@
"common": "*",
"config": "*",
"date-fns": "2.10.0",
"dayjs": "^1.11.4",
"dayjs": "^1.11.5",
"framer-motion": "^6.5.1",
"globby": "^12.0.2",
"gray-matter": "^4.0.2",
"markdown-toc": "^1.2.0",
"marked": "^4.0.12",
"next": "12.1.0",
"next": "12.3.2",
"next-compose-plugins": "^2.2.1",
"next-mdx-remote": "^4.1.0",
"next-seo": "^4.20.0",
+1 -1
View File
@@ -320,7 +320,7 @@ function BlogPostPage(props: any) {
</div>
{/* Sidebar */}
<div className="col-span-12 space-y-8 lg:col-span-5 xl:col-span-3 xl:col-start-9">
<div className="space-y-8 lg:sticky lg:top-16 lg:mb-16">
<div className="space-y-8 lg:sticky lg:top-24 lg:mb-24">
<div className="hidden lg:block">{toc}</div>
<div>
<div className="mb-4">
+3 -8
View File
@@ -110,14 +110,9 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
<p className="text-sm md:text-base text-scale-1000 max-w-sm sm:max-w-md md:max-w-lg mx-auto">
Explore remote possibilities and join our team to help us achieve it.
</p>
<Button
as="a"
// @ts-ignore
href="#positions"
className="text-white xl:text-sm"
>
Open positions
</Button>
<a href="#positions">
<Button className="text-white xl:text-sm">Open positions</Button>
</a>
</div>
<SectionContainer>
@@ -0,0 +1,46 @@
.toc a {
text-decoration: none !important;
font-weight: 400 !important;
}
.toc > ul > li {
padding-left: 0 !important;
}
.toc li {
@apply text-sm !important;
padding-left: 1rem !important;
}
.toc ul > li:before {
content: '';
position: absolute;
background-color: none;
border-radius: 50%;
width: 0 !important;
height: 0 !important;
top: 0.6875em;
left: 0.25em;
}
.header:hover svg,
.header svg:hover {
display: inline-block;
}
.article h1,
.article h2,
.article h3,
.article h4,
.article h5 {
font-weight: 500 !important;
}
.article {
@apply mb-16;
}
.article > div > pre {
padding: 0;
background: none;
}
+212
View File
@@ -0,0 +1,212 @@
import { Button, IconChevronRight, IconExternalLink } from '@supabase/ui'
import matter from 'gray-matter'
import { MDXRemote } from 'next-mdx-remote'
import { NextSeo } from 'next-seo'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { IconChevronLeft, IconChevronsLeft } from '~/../../packages/ui'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
import mdxComponents from '~/lib/mdx/mdxComponents'
import { mdxSerialize } from '~/lib/mdx/mdxSerialize'
import { getAllPostSlugs, getPostdata, getSortedPosts } from '~/lib/posts'
// table of contents extractor
const toc = require('markdown-toc')
export async function getStaticPaths() {
const paths = getAllPostSlugs('_customers')
return {
paths,
fallback: false,
}
}
export async function getStaticProps({ params }: any) {
const filePath = `${params.slug}`
const postContent = await getPostdata(filePath, '_customers')
const { data, content } = matter(postContent)
const mdxSource: any = await mdxSerialize(content)
const relatedPosts = getSortedPosts('_customers', 5, mdxSource.scope.tags)
const allPosts = getSortedPosts('_customers')
const currentIndex = allPosts
.map(function (e) {
return e.slug
})
.indexOf(filePath)
const nextPost = allPosts[currentIndex + 1]
const prevPost = allPosts[currentIndex - 1]
const payload = {
props: {
prevPost: currentIndex === 0 ? null : prevPost ? prevPost : null,
nextPost: currentIndex === allPosts.length ? null : nextPost ? nextPost : null,
relatedPosts,
blog: {
slug: `${params.slug}`,
content: mdxSource,
source: content,
...data,
toc: toc(content, { maxdepth: data.toc_depth ? data.toc_depth : 2 }),
},
},
}
return payload
}
function CaseStudyPage(props: any) {
const content = props.blog.content
const { basePath } = useRouter()
const meta = {
title: `${props.blog.name} | Supabase Customer Stories`,
description: props.blog.description,
image: props.blog.og_image ?? `${basePath}/images/customers/og/customer-stories.jpg`,
url: `https://supabase.io/customers/${props.blog.slug}`,
}
return (
<>
<NextSeo
title={meta.title}
openGraph={{
title: meta.title,
description: meta.description,
url: meta.url,
type: 'article',
article: {
//
// to do: add expiration and modified dates
// https://github.com/garmeeh/next-seo#article
publishedTime: props.blog.date,
},
images: [
{
url: meta.image,
},
],
}}
/>
<DefaultLayout>
<div
className="
container mx-auto px-8 py-16 sm:px-16
xl:px-20
"
>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-12 mb-2 lg:col-span-2">
{/* Back button */}
<p>
<a
href={'/customers'}
className="text-scale-900 hover:text-scale-1200 flex cursor-pointer items-center text-sm transition"
>
<IconChevronLeft style={{ padding: 0 }} />
Back
</a>
</p>
</div>
<div
className="col-span-12 lg:col-span-8
"
>
<div className="">
<article className="flex flex-col gap-8">
<div className="flex flex-col gap-8 max-w-xxl">
<Link passHref href="/customers">
<a className="text-brand-900 hover:text-brand-1000 mb-2 mt-0">
Customer Stories
</a>
</Link>
<h1 className="text-scale-1200 text-4xl font-semibold xl:text-5xl">
{props.blog.title}
</h1>
<h2 className="text-scale-1200 text-xl xl:text-2xl">
{props.blog.description}
</h2>
</div>
<div className="grid grid-cols-12 prose max-w-none gap-8 lg:gap-20">
<div className="col-span-12 lg:col-span-4 lg:block xl:col-span-4">
<div className="space-y-8 lg:sticky lg:top-24 lg:mb-24">
{/* Logo */}
<div className={`relative h-16 w-32`}>
<p className="flex flex-row ">
<Image
layout="fill"
src={`${props.blog.logo}`}
alt={`${props.blog.title} logo`}
objectFit="scale-down"
objectPosition="left"
className="
bg-no-repeat
dark:brightness-200
dark:contrast-0
dark:filter
"
/>
</p>
</div>
<div className="flex flex-col space-y-2">
<span className="text-scale-900">About</span>
<p>{props.blog.about}</p>
<span className="not-prose ">
<a
href={props.blog.company_url}
className=" flex cursor-pointer items-center space-x-1 opacity-50 transition-opacity hover:opacity-100"
target="_blank"
>
<span>{props.blog.company_url}</span>
<IconExternalLink size={14} />
</a>
</span>
</div>
{props.blog.misc.map((x: any) => {
return (
<div className="flex flex-col gap-0">
<span className="text-scale-900">{x.label}</span>
<span className="text-scale-1100">{x.text}</span>
</div>
)
})}
<div className="">
<p>Ready to get started?</p>
<div>
<Link href="https://supabase.com/contact/enterprise">
<a className="no-underline">
<Button type="default" iconRight={<IconChevronRight />}>
Contact sales
</Button>
</a>
</Link>
</div>
</div>
</div>
</div>
<div className="xm:col-span-7 col-span-12 lg:col-span-8 xl:col-span-8 ">
<MDXRemote {...content} components={mdxComponents()} />
</div>
</div>
</article>
</div>
</div>
</div>
</div>
<CTABanner />
</DefaultLayout>
</>
)
}
export default CaseStudyPage
@@ -0,0 +1,65 @@
.bg-visual {
animation: fadeIn 3s ease-out both;
background-image:
/* Bg glow */ radial-gradient(
80% 100% at 50% 75%,
#3ecf8e10 0%,
#0d0e1200 100%
),
radial-gradient(40% 70% at 50% 80%, #3ecf8e20 0%, #3ecf8e00 100%),
/* radial-gradient(100% 100% at 50% 80%, #1c1c1c50 0%, #3ecf8e00 100%), */
/* Stars from left to right =================================== */
radial-gradient(1px 1px at 5% 25%, #3ecf8e70 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 14% 15%, #3ecf8e 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 19% 35%, #3ecf8e 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 29% 10%, #3ecf8e 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 34% 25%, #3ecf8e80 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 38% 38%, #3ecf8e 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 42% 19%, #3ecf8e80 0%, #3ecf8e00 100%),
radial-gradient(2px 2px at 49% 27%, #3ecf8e90 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 52% 7%, #3ecf8e75 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 55% 21%, #3ecf8e90 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 59% 27%, #3ecf8e 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 61% 14%, #3ecf8e90 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 64% 44%, #3ecf8e 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 74% 15%, #3ecf8e 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 80% 36%, #3ecf8e 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 84% 35%, #3ecf8e 0%, #3ecf8e00 100%),
radial-gradient(2px 2px at 94% 35%, #3ecf8e50 0%, #3ecf8e00 100%),
radial-gradient(1px 1px at 90% 20%, #3ecf8e 0%, #3ecf8e00 100%);
}
.bg-visual::after {
content: ' ';
position: absolute;
margin: 0 auto;
left: -20vw;
right: -20vw;
bottom: -65%;
width: 140vw;
height: 130%;
border-radius: 100%;
}
.bg-visual::before {
content: ' ';
position: absolute;
margin: 0 auto;
left: -18vw;
right: -18vw;
bottom: calc(-65% + 2px);
width: 136vw;
height: 130%;
border-radius: 100%;
background: #7fe5b794;
box-shadow: 0 -52px 0 -30vw #ffffff;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
+154
View File
@@ -0,0 +1,154 @@
import fs from 'fs'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import Head from 'next/head'
import { NextSeo } from 'next-seo'
import { generateRss } from '~/lib/rss'
import { getSortedPosts, getAllCategories } from '~/lib/posts'
import DefaultLayout from '~/components/Layouts/Default'
import PostTypes from '~/types/post'
import { motion } from 'framer-motion'
import styles from './customers.module.css'
import Link from 'next/link'
import { GlassPanel } from 'ui'
export async function getStaticProps() {
const allPostsData = getSortedPosts('_customers')
const categories = getAllCategories('_customers')
const rss = generateRss(allPostsData)
// create a rss feed in public directory
// rss feed is added via <Head> component in render return
fs.writeFileSync('./public/customers-rss.xml', rss)
return {
props: {
blogs: allPostsData,
categories,
},
}
}
function CustomerStoriesPage(props: any) {
const [category, setCategory] = useState('all')
const [blogs, setBlogs] = useState(props.blogs)
const { basePath } = useRouter()
const meta = {
title: 'Customer Stories | Supabase',
image: `${basePath}/images/customers/og/customer-stories.jpg`,
description:
'See how Supabase empowers companies of all sizes to accelerate their growth and streamline their work.',
}
useEffect(() => {
// Update the document title using the browser API
setBlogs(
category === 'all'
? props.blogs
: props.blogs.filter((post: any) => {
const found = post.tags.includes(category)
return found
})
)
}, [category])
const caseStudyThumbs = blogs.map((blog: PostTypes, idx: number) => {
return {
logo: blog.logo,
logoInverse: blog.logo_inverse,
title: blog.title,
link: blog.url,
}
})
return (
<>
<Head>
<title>{meta.title}</title>
<meta name="description" content={meta.description} />
<meta property="og:image" content={meta.image} />
<meta name="twitter:image" content={meta.image} />
<link
rel="alternate"
type="application/rss+xml"
title="RSS feed for customer stories"
href={`${basePath}/customers-rss.xml`}
/>
</Head>
<NextSeo
title={meta.title}
description={meta.description}
openGraph={{
title: meta.title,
description: meta.description,
url: `${basePath}/customers`,
images: [
{
url: meta.image,
},
],
}}
/>
<DefaultLayout>
<div className="relative z-0 dark:bg-scale-200 bg-scale-200 overflow-hidden">
<div className="container mx-auto mt-44 px-8 sm:px-4 xl:px-20">
<div className="mx-auto relative z-10">
<motion.div
className="mx-auto max-w-2xl text-center"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0, transition: { duration: 0.5, easing: 'easeOut' } }}
>
<h1 className="text-scale-1200 mb-3 text-3xl">Customer stories</h1>
<h2 className="text-scale-1100 text-xl">
Discover how Supabase is being used around the world to quickly create outstanding
products and set new industry standards.
</h2>
</motion.div>
<div className="mx-auto my-12 md:my-20 grid grid-cols-12 gap-6 not-prose">
{caseStudyThumbs.map((caseStudy: any, i: number) => (
<Link href={`${caseStudy.link}`} key={caseStudy.title} passHref>
<motion.a
className="col-span-12 md:col-span-4"
initial={{ opacity: 0, y: 20 }}
animate={{
opacity: 1,
y: 0,
transition: {
duration: 0.4,
ease: [0.24, 0.25, 0.05, 1],
delay: 0.2 + i / 15,
},
}}
>
<GlassPanel
{...caseStudy}
background={true}
showIconBg={true}
showLink={true}
hasLightIcon
>
{caseStudy.description}
</GlassPanel>
</motion.a>
</Link>
))}
</div>
</div>
<div
className={[
'absolute inset-0 h-[300px] dark:bg-scale-200 bg-bg-scale-200 z-0 after:!bg-scale-200',
styles['bg-visual'],
].join(' ')}
/>
</div>
</div>
</DefaultLayout>
</>
)
}
export default CustomerStoriesPage
+16 -4
View File
@@ -1,3 +1,5 @@
import { getSortedPosts, getAllCategories } from '~/lib/posts'
import PostTypes from '~/types/post'
import Container from 'components/Container'
import Layout from '~/components/Layouts/Default'
import Hero from 'components/Hero'
@@ -6,16 +8,16 @@ import Features from 'components/Features/index'
import BuiltExamples from 'components/BuiltWithSupabase/index'
import MadeForDevelopers from 'components/MadeForDevelopers/index'
import AdminAccess from 'components/AdminAccess/index'
import CaseStudies from 'components/CaseStudies/index'
import CTABanner from 'components/CTABanner/index'
import CustomerStories from 'components/CustomerStories'
import TwitterSocialSection from '~/components/TwitterSocialSection'
// Import Swiper styles if swiper used on page
import 'swiper/swiper.min.css'
type Props = {}
type Props = { customerStories: PostTypes[] }
const Index = ({}: Props) => {
const Index = ({ customerStories }: Props) => {
return (
<>
<Layout>
@@ -26,7 +28,7 @@ const Index = ({}: Props) => {
<BuiltExamples />
<MadeForDevelopers />
<AdminAccess />
<CaseStudies />
<CustomerStories customerStories={customerStories} />
<CTABanner />
</Container>
</Layout>
@@ -34,4 +36,14 @@ const Index = ({}: Props) => {
)
}
export async function getStaticProps() {
const customerStories = getSortedPosts('_customers', 3)
return {
props: {
customerStories,
},
}
}
export default Index
+5 -3
View File
@@ -148,9 +148,11 @@ export default function TicketsPage({ users }: Props) {
</p>
<div className="mt-1">
<Link href="/launch-week">
<Button as="a" type="outline" size="medium">
Get your ticket now
</Button>
<a>
<Button type="outline" size="medium">
Get your ticket now
</Button>
</a>
</Link>
</div>
</motion.div>
+2 -2
View File
@@ -12,7 +12,7 @@ import supabase from '~/lib/supabase'
import { Partner } from '~/types/partners'
import Error404 from '../404'
function Partner({ partner }: { partner: Partner }) {
function PartnerPage({ partner }: { partner: Partner }) {
if (!partner) return <Error404 />
return (
<>
@@ -243,4 +243,4 @@ export const getStaticProps: GetStaticProps = async ({ params }) => {
}
}
export default Partner
export default PartnerPage
+47
View File
@@ -0,0 +1,47 @@
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Blog - Supabase</title>
<link>https://supabase.com</link>
<description>Latest news from Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 15 Feb 2023 23:11:00 +0000</lastBuildDate>
<atom:link href="https://supabase.com/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/case-studies/happyteams</guid>
<title>HappyTeams unlocks better performance and reduces cost with Supabase.</title>
<link>https://supabase.com/case-studies/happyteams</link>
<description>How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back.</description>
<pubDate>Wed, 15 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/case-studies/xendit</guid>
<title>Xendit use Supabase and create a full solution shipped to production in less than one week.</title>
<link>https://supabase.com/case-studies/xendit</link>
<description>As a payment processor, Xendit are responsible for verifying that all transactions are legal.</description>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/case-studies/replenysh</guid>
<title>Replenysh uses Supabase to implement OTP in less than 24 hours.</title>
<link>https://supabase.com/case-studies/replenysh</link>
<description>With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres.</description>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/case-studies/mobbin</guid>
<title>How Mobbin migrated 200,000 users from Firebase for a better authentication experience.</title>
<link>https://supabase.com/case-studies/mobbin</link>
<description>Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps.</description>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/case-studies/epsilon3</guid>
<title>Epsilon3 digitize paper-based procedures in the space industry using telemetry data, simplifying testing and operations.</title>
<link>https://supabase.com/case-studies/epsilon3</link>
<description>Epsilon3 uses Supabase to help teams execute secure and reliable operations in an industry where project spend runs into the billions.</description>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
</channel>
</rss>
+47
View File
@@ -0,0 +1,47 @@
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Blog - Supabase</title>
<link>https://supabase.com</link>
<description>Latest news from Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 15 Feb 2023 03:03:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/customers/happyteams</guid>
<title>HappyTeams unlocks better performance and reduces cost with Supabase.</title>
<link>https://supabase.com/customers/happyteams</link>
<description>How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back.</description>
<pubDate>Wed, 15 Feb 2023 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/xendit</guid>
<title>Xendit use Supabase and create a full solution shipped to production in less than one week.</title>
<link>https://supabase.com/customers/xendit</link>
<description>As a payment processor, Xendit are responsible for verifying that all transactions are legal.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/replenysh</guid>
<title>Replenysh uses Supabase to implement OTP in less than 24 hours.</title>
<link>https://supabase.com/customers/replenysh</link>
<description>With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/mobbin</guid>
<title>How Mobbin migrated 200,000 users from Firebase for a better authentication experience.</title>
<link>https://supabase.com/customers/mobbin</link>
<description>Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/epsilon3</guid>
<title>Epsilon3 digitize paper-based procedures in the space industry using telemetry data, simplifying testing and operations.</title>
<link>https://supabase.com/customers/epsilon3</link>
<description>Epsilon3 uses Supabase to help teams execute secure and reliable operations in an industry where project spend runs into the billions.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
</item>
</channel>
</rss>
Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Loaded 100 of 301 files, more files were not shown because too many files have changed in this diff. Show more