No files matched your search
@@ -142,7 +142,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-java" target="_blank" rel="noopener noreferrer">gotrue-java</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/storage-java" target="_blanke" rel="noopener noreferrer">storage-java</a></td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -5158,9 +5158,9 @@
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
||||
},
|
||||
"decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "3.3.0",
|
||||
|
||||
@@ -20,7 +20,6 @@ export function SearchProvider({ children }: any) {
|
||||
const router = useRouter()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [initialQuery, setInitialQuery] = useState(null)
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
setIsOpen(true)
|
||||
}, [setIsOpen])
|
||||
@@ -85,6 +84,7 @@ export function SearchProvider({ children }: any) {
|
||||
hitComponent={Hit}
|
||||
transformItems={(items) => {
|
||||
return items.map((item, index) => {
|
||||
console.log('item', item)
|
||||
// We transform the absolute URL into a relative URL to
|
||||
// leverage Next's preloading.
|
||||
const a = document.createElement('a')
|
||||
@@ -96,7 +96,8 @@ export function SearchProvider({ children }: any) {
|
||||
...item,
|
||||
url: `${a.pathname}${hash}`,
|
||||
__is_result: () => true,
|
||||
__is_parent: () => item.type === 'lvl1' && items.length > 1 && index === 0,
|
||||
__is_parent: () => item.type === 'lvl1' && index === 0,
|
||||
__is_guide: () => item.hierarchy.lvl0 === 'Guides',
|
||||
__is_child: () =>
|
||||
item.type !== 'lvl1' &&
|
||||
items.length > 1 &&
|
||||
@@ -125,6 +126,7 @@ function Hit({ hit, children }) {
|
||||
'DocSearch-Hit--FirstChild': hit.__is_first?.(),
|
||||
'DocSearch-Hit--LastChild': hit.__is_last?.(),
|
||||
'DocSearch-Hit--Child': hit.__is_child?.(),
|
||||
'DocSearch-Hit--Guide': hit.__is_guide?.(),
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useTheme } from 'common/Providers'
|
||||
import { IconSun, IconMoon } from '@supabase/ui'
|
||||
|
||||
function DarkModeToggle() {
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
localStorage.setItem('supabaseDarkMode', (!isDarkMode).toString())
|
||||
toggleTheme()
|
||||
|
||||
const key = localStorage.getItem('supabaseDarkMode')
|
||||
document.documentElement.className = key === 'true' ? 'dark' : ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<IconSun className="text-scale-900" strokeWidth={2} />
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed="false"
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
|
||||
transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
isDarkMode ? 'bg-scale-500' : 'bg-scale-900'
|
||||
} mx-5
|
||||
`}
|
||||
onClick={() => toggleDarkMode()}
|
||||
>
|
||||
<span className="sr-only">Toggle Themes</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`
|
||||
${
|
||||
isDarkMode ? 'translate-x-5' : 'translate-x-0'
|
||||
} dark:bg-scale-300 inline-block h-5 w-5
|
||||
transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
<IconMoon className="text-scale-900" strokeWidth={2} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DarkModeToggle
|
||||
@@ -1,133 +0,0 @@
|
||||
import { Badge } from 'ui'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import FooterLinks from 'data/footer.json'
|
||||
import DarkModeToggle from './DarkModeToggle'
|
||||
import { useTheme } from 'common/Providers'
|
||||
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
|
||||
import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png'
|
||||
|
||||
const Footer = () => {
|
||||
const { isDarkMode } = useTheme()
|
||||
|
||||
return (
|
||||
<footer
|
||||
className="border-scale-500 dark:border-scale-600 border-t"
|
||||
aria-labelledby="footerHeading"
|
||||
>
|
||||
<h2 id="footerHeading" className="sr-only">
|
||||
Footer
|
||||
</h2>
|
||||
<div className="sm:py-18 container relative mx-auto px-6 py-16 md:py-24 lg:px-16 lg:py-24 xl:px-20">
|
||||
<div className="xl:grid xl:grid-cols-3 xl:gap-8">
|
||||
<div className="space-y-8 xl:col-span-1">
|
||||
<Link href="#" as="/">
|
||||
<a className="w-40">
|
||||
<Image
|
||||
src={isDarkMode ? supabaseLogoWordmarkDark : supabaseLogoWordmarkLight}
|
||||
width={160}
|
||||
height={30}
|
||||
alt="Supabase"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex space-x-5">
|
||||
<a
|
||||
href="https://twitter.com/supabase"
|
||||
className="text-scale-900 hover:text-scale-1200 transition"
|
||||
>
|
||||
<span className="sr-only">Twitter</span>
|
||||
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/supabase"
|
||||
className="text-scale-900 hover:text-scale-1200 transition"
|
||||
>
|
||||
<span className="sr-only">GitHub</span>
|
||||
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://discord.supabase.com/"
|
||||
className="text-scale-900 hover:text-scale-1200 transition"
|
||||
>
|
||||
<span className="sr-only">Discord</span>
|
||||
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 71 55" aria-hidden="true">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://youtube.com/c/supabase"
|
||||
className="text-scale-900 hover:text-scale-1200 transition"
|
||||
>
|
||||
<span className="sr-only">Youtube</span>
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 576 512"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-12 grid grid-cols-1 gap-8 xl:col-span-2 xl:mt-0">
|
||||
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
|
||||
{FooterLinks.map((segment: any) => {
|
||||
return (
|
||||
<div key={`footer_${segment.title}`}>
|
||||
<h6 className="text-scale-1200 overwrite text-base">{segment.title}</h6>
|
||||
<ul className="mt-4 space-y-2">
|
||||
{segment.links.map((link: any, idx: number) => (
|
||||
<li key={`${segment.title}_link_${idx}`}>
|
||||
<a
|
||||
href={link.url}
|
||||
className={`text-sm transition-colors ${
|
||||
link.url
|
||||
? 'text-scale-1100 hover:text-scale-1200 '
|
||||
: 'text-scale-900 hover:text-scale-900'
|
||||
} `}
|
||||
>
|
||||
{link.text}
|
||||
{!link.url && (
|
||||
<div className="ml-2 inline text-xs xl:ml-0 xl:block 2xl:ml-2 2xl:inline">
|
||||
<Badge color="scale" size="small">
|
||||
Coming soon
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-scale-500 dark:border-scale-600 mt-32 flex justify-between border-t pt-8">
|
||||
<small className="small">© Supabase Inc</small>
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
@@ -1,11 +1,13 @@
|
||||
const FooterHelpCallout = () => {
|
||||
return (
|
||||
<div className="min-w-full px-8 py-6 prose bg-scale-300 dark:bg-whiteA-200 rounded">
|
||||
<h5 className="text-sm m-0 text-scale-1100">Need some help?</h5>
|
||||
<p className="m-0 text-sm text-scale-900">
|
||||
Not to worry, our specialist engineers are here to help. Submit a support ticket through the{' '}
|
||||
<a href="https://app.supabase.com/support/new">Dashboard</a>.
|
||||
</p>
|
||||
<div className="mt-32">
|
||||
<div className="min-w-full px-8 py-6 prose bg-scale-300 dark:bg-whiteA-200 rounded">
|
||||
<h5 className="text-sm m-0 text-scale-1100">Need some help?</h5>
|
||||
<p className="m-0 text-sm text-scale-900">
|
||||
Not to worry, our specialist engineers are here to help. Submit a support ticket through
|
||||
the <a href="https://app.supabase.com/support/new">Dashboard</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Button, IconGitHub, IconTwitter } from '~/../../packages/ui'
|
||||
import footerData from '~/data/footer.json'
|
||||
import { useTheme } from 'common/Providers'
|
||||
|
||||
const Footer = () => {
|
||||
const { isDarkMode } = useTheme()
|
||||
return (
|
||||
<div>
|
||||
<hr className="border-scale-400 mt-8"></hr>
|
||||
<div className="flex gap-4 items-center mt-6 justify-between">
|
||||
<div className="flex flex-col lg:flex-row gap-3 ">
|
||||
<span className="text-xs text-scale-900">Supabase 2022</span>
|
||||
<span className="text-xs text-scale-900">—</span>
|
||||
{footerData.map((item) => (
|
||||
<Link href={item.url}>
|
||||
<a className="text-xs text-scale-800 hover:underline">{item.title}</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="text"
|
||||
as="a"
|
||||
// @ts-ignore
|
||||
href="https://twitter.com/supabase"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<IconTwitter size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
as="a"
|
||||
// @ts-ignore
|
||||
href="https://discord.supabase.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="16">
|
||||
<path
|
||||
fill="none"
|
||||
fill-rule="nonzero"
|
||||
stroke={`${isDarkMode ? '#fff' : '#333'}`}
|
||||
d="M16.238 2.16a14.628 14.628 0 0 0-3.664-1.159.055.055 0 0 0-.059.028c-.158.288-.333.662-.456.957a13.432 13.432 0 0 0-4.115 0 9.76 9.76 0 0 0-.464-.957.057.057 0 0 0-.058-.028 14.587 14.587 0 0 0-3.664 1.16.053.053 0 0 0-.024.02C1.4 5.74.76 9.21 1.074 12.637c.002.017.011.033.024.043a14.813 14.813 0 0 0 4.494 2.318.058.058 0 0 0 .064-.02c.346-.483.654-.992.92-1.527a.059.059 0 0 0-.032-.08c-.49-.19-.956-.421-1.404-.684a.06.06 0 0 1-.006-.097c.094-.072.189-.148.279-.223a.055.055 0 0 1 .058-.008c2.946 1.372 6.135 1.372 9.046 0a.055.055 0 0 1 .06.007 7.2 7.2 0 0 0 .279.224.06.06 0 0 1-.005.097 9.143 9.143 0 0 1-1.405.683.06.06 0 0 0-.03.081c.27.534.578 1.043.918 1.526.014.02.04.029.063.021A14.763 14.763 0 0 0 18.9 12.68a.06.06 0 0 0 .023-.042c.376-3.962-.628-7.404-2.66-10.455a.046.046 0 0 0-.024-.021Zm-9.223 8.39c-.887 0-1.618-.831-1.618-1.852 0-1.02.717-1.851 1.618-1.851.908 0 1.632.838 1.618 1.851 0 1.02-.717 1.851-1.618 1.851Zm5.981 0c-.887 0-1.618-.831-1.618-1.852 0-1.02.717-1.851 1.618-1.851.908 0 1.632.838 1.618 1.851 0 1.02-.71 1.851-1.618 1.851Z"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
@@ -64,6 +64,12 @@ const home = [
|
||||
href: '/guides/resources',
|
||||
level: 'resources',
|
||||
},
|
||||
{
|
||||
label: 'Self-Hosting',
|
||||
icon: '/img/icons/menu/platform',
|
||||
href: '/guides/self-hosting',
|
||||
level: 'self_hosting',
|
||||
},
|
||||
{
|
||||
label: 'Integrations',
|
||||
icon: '/img/icons/menu/integrations',
|
||||
|
||||
@@ -298,6 +298,7 @@ export const auth = {
|
||||
url: '/guides/auth/social-login',
|
||||
items: [...SocialLoginItems],
|
||||
},
|
||||
{ name: 'Email Templates', url: '/guides/auth/auth-email-templates', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -468,6 +469,7 @@ export const platform = {
|
||||
items: [
|
||||
{ name: 'Compute Add-ons', url: '/guides/platform/compute-add-ons', items: [] },
|
||||
{ name: 'Custom Domains', url: '/guides/platform/custom-domains', items: [] },
|
||||
{ name: 'Database Backups', url: '/guides/platform/backups', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -568,45 +570,37 @@ export const resources = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Self-Hosting',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/resources/self-hosting', items: [] },
|
||||
{ name: 'Docker', url: '/guides/resources/self-hosting/docker', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Self-Hosting Reference',
|
||||
items: [
|
||||
{
|
||||
name: 'Auth Server',
|
||||
url: '/reference/self-hosting-auth/introduction',
|
||||
icon: '/img/icons/menu/reference-auth',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Storage Server',
|
||||
url: '/reference/self-hosting-storage/introduction',
|
||||
icon: '/img/icons/menu/reference-storage',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Realtime Server',
|
||||
url: '/reference/self-hosting-realtime/introduction',
|
||||
icon: '/img/icons/menu/reference-realtime',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const selfHosting = {
|
||||
label: 'Self-Hosting',
|
||||
export const self_hosting = {
|
||||
title: 'Self-Hosting',
|
||||
icon: 'resources',
|
||||
url: '/guides/self-hosting',
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/hosting/overview', items: [] },
|
||||
{ name: 'Docker', url: '/guides/hosting/docker', items: [] },
|
||||
{ name: 'Overview', url: '/guides/self-hosting', items: [] },
|
||||
{ name: 'Self-Hosting with Docker', url: '/guides/self-hosting/docker', items: [] },
|
||||
{
|
||||
name: 'Auth Server',
|
||||
items: [
|
||||
{ name: 'Reference', url: '/reference/self-hosting-auth/introduction', items: [] },
|
||||
{ name: 'Configuration', url: '/guides/self-hosting/auth/config', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Storage Server',
|
||||
items: [
|
||||
{ name: 'Reference', url: '/reference/self-hosting-storage/introduction', items: [] },
|
||||
{ name: 'Configuration', url: '/guides/self-hosting/storage/config', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Realtime Server',
|
||||
items: [
|
||||
{ name: 'Reference', url: '/reference/self-hosting-realtime/introduction', items: [] },
|
||||
{ name: 'Configuration', url: '/guides/self-hosting/realtime/config', items: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -674,6 +668,11 @@ export const integrations = {
|
||||
{ name: 'Plasmic', url: '/guides/integrations/plasmic', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Messaging',
|
||||
url: undefined,
|
||||
items: [{ name: 'OneSignal', url: '/guides/integrations/onesignal', items: [] }],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,9 @@ const SideNav = () => {
|
||||
case url.includes(`/docs/guides/resources`) && url:
|
||||
menuState.setMenuLevelId('resources')
|
||||
break
|
||||
case url.includes(`/docs/guides/self-hosting`) && url:
|
||||
menuState.setMenuLevelId('self_hosting')
|
||||
break
|
||||
case url.includes(`/docs/guides/integrations`) && url:
|
||||
menuState.setMenuLevelId('integrations')
|
||||
break
|
||||
@@ -166,6 +169,7 @@ const SideNav = () => {
|
||||
<NavigationMenuGuideList id={'platform'} />
|
||||
<NavigationMenuGuideList id={'resources'} />
|
||||
<NavigationMenuGuideList id={'integrations'} />
|
||||
<NavigationMenuGuideList id={'self_hosting'} />
|
||||
<NavigationMenuGuideList id={'reference'} />
|
||||
{/* // Client Libs */}
|
||||
<NavigationMenuRefList
|
||||
|
||||
@@ -73,7 +73,6 @@ const NavigationMenuGuideList: React.FC<Props> = ({ id, setMenuLevelId }) => {
|
||||
`/img/icons/menu/${menu.icon}${isDarkMode ? '' : '-light'}.svg`
|
||||
}
|
||||
/>
|
||||
{/* </div> */}
|
||||
<span
|
||||
className={[
|
||||
' ',
|
||||
|
||||
@@ -2,19 +2,7 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Dropdown,
|
||||
IconCommand,
|
||||
IconMenu,
|
||||
IconMessageSquare,
|
||||
IconMoon,
|
||||
IconSearch,
|
||||
IconSun,
|
||||
Input,
|
||||
Listbox,
|
||||
} from 'ui'
|
||||
import { Button, IconCommand, IconGitHub, IconMoon, IconSearch, IconSun } from 'ui'
|
||||
import { SearchButton } from '~/components/DocSearch'
|
||||
import { REFERENCES } from '~/components/Navigation/Navigation.constants'
|
||||
import { useTheme } from 'common/Providers'
|
||||
@@ -134,7 +122,7 @@ const TopNavBarRef: FC = () => {
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Main site
|
||||
Supabase.com
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
@@ -146,6 +134,18 @@ const TopNavBarRef: FC = () => {
|
||||
>
|
||||
Dashboard
|
||||
</Button>
|
||||
<ul className="flex items-center">
|
||||
<Button
|
||||
type="text"
|
||||
as="a"
|
||||
// @ts-ignore
|
||||
href="https://github.com/supabase/supabase"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<IconGitHub size={16} />
|
||||
</Button>
|
||||
</ul>
|
||||
<ul className="flex items-center">
|
||||
<li className="px-4">
|
||||
<div className="cursor-pointer" onClick={toggleDarkMode}>
|
||||
|
||||
@@ -27,6 +27,10 @@ import RefHeaderSection from './reference/RefHeaderSection'
|
||||
// Ref version specific
|
||||
import CliGlobalFlagsHandler from '~/components/reference/enrichments/cli/CliGlobalFlagsHandler'
|
||||
|
||||
import Options from '~/components/Options'
|
||||
import Param from '~/components/Params'
|
||||
import Image from 'next/image'
|
||||
|
||||
const components = {
|
||||
Admonition,
|
||||
Button,
|
||||
@@ -68,6 +72,15 @@ const components = {
|
||||
code: (props: any) => <CodeBlock {...props} />,
|
||||
RefHeaderSection: (props: any) => <RefHeaderSection {...props} />,
|
||||
CliGlobalFlagsHandler: () => <CliGlobalFlagsHandler />,
|
||||
Options,
|
||||
Param,
|
||||
img: (props: any) => {
|
||||
return (
|
||||
<span className={['next-image--dynamic-fill'].join(' ')}>
|
||||
<Image {...props} className={['rounded-md border'].join(' ')} layout="fill" />
|
||||
</span>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default components
|
||||
@@ -27,6 +27,7 @@ export type Flag = {
|
||||
description: string
|
||||
default_value: string
|
||||
accepted_values: AcceptedValue[]
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export type AcceptedValue = {
|
||||
@@ -107,7 +108,7 @@ const CliCommandSection = (props) => {
|
||||
{command.flags.map((flag: Flag) => (
|
||||
<>
|
||||
<li className="mt-0">
|
||||
<Param {...flag}>
|
||||
<Param {...flag} isOptional={!flag.required}>
|
||||
{flag?.accepted_values && (
|
||||
<Options>
|
||||
{flag?.accepted_values.map((value) => {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
function RefSEO({ title }) {
|
||||
const router = useRouter()
|
||||
|
||||
const path = router.asPath.replace('crawlers/', '')
|
||||
|
||||
return (
|
||||
<NextSeo
|
||||
title={title}
|
||||
openGraph={{
|
||||
title,
|
||||
url: `https://supabase.com/docs${path}`,
|
||||
images: [
|
||||
{
|
||||
url: `https://supabase.com/docs/img/supabase-og-image.png`,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default RefSEO
|
||||
@@ -5,7 +5,7 @@ import { useEffect } from 'react'
|
||||
import components from '~/components'
|
||||
import RefEducationSection from '~/components/reference/RefEducationSection'
|
||||
import RefFunctionSection from '~/components/reference/RefFunctionSection'
|
||||
import OldLayout from '~/layouts/Default'
|
||||
|
||||
import RefSubLayout from '~/layouts/ref/RefSubLayout'
|
||||
import ApiOperationSection from './ApiOperationSection'
|
||||
import CliCommandSection from './CLICommandSection'
|
||||
@@ -34,18 +34,6 @@ const RefSectionHandler = (props) => {
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* handle old ref pages
|
||||
*/
|
||||
if (!isNewDocs) {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<OldLayout meta={props.pageProps.meta} toc={props.pageProps.toc}>
|
||||
<MDXRemote {...props.pageProps.content} components={components} />
|
||||
</OldLayout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<RefSubLayout>
|
||||
{props.sections.map((x) => {
|
||||
|
||||
@@ -1,26 +1,8 @@
|
||||
const authProviders = [
|
||||
// {
|
||||
// name: 'Email',
|
||||
// // logo: '/img/libraries/dart-icon.svg',
|
||||
// href: '/docs/guides/auth/auth-apple',
|
||||
// official: true,
|
||||
// supporter: 'Supabase',
|
||||
// platform: true,
|
||||
// selfHosted: true,
|
||||
// },
|
||||
// {
|
||||
// name: 'Magic Links',
|
||||
// // logo: '/img/libraries/dart-icon.svg',
|
||||
// href: '/docs/guides/auth/auth-apple',
|
||||
// official: true,
|
||||
// supporter: 'Supabase',
|
||||
// platform: true,
|
||||
// selfHosted: true,
|
||||
// },
|
||||
{
|
||||
name: 'Apple',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-apple',
|
||||
href: '/guides/auth/social-login/auth-apple',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -29,7 +11,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Azure',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-azure',
|
||||
href: '/guides/auth/social-login/auth-azure',
|
||||
official: false,
|
||||
supporter: 'TBD',
|
||||
platform: true,
|
||||
@@ -38,7 +20,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Bitbucket',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-bitbucket',
|
||||
href: '/guides/auth/social-login/auth-bitbucket',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -47,7 +29,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Discord',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-discord',
|
||||
href: '/guides/auth/social-login/auth-discord',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -56,7 +38,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Facebook',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-facebook',
|
||||
href: '/guides/auth/social-login/auth-facebook',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -65,7 +47,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'GitHub',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-github',
|
||||
href: '/guides/auth/social-login/auth-github',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -74,7 +56,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'GitLab',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-gitlab',
|
||||
href: '/guides/auth/social-login/auth-gitlab',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -83,7 +65,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Google',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-google',
|
||||
href: '/guides/auth/social-login/auth-google',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -91,7 +73,7 @@ const authProviders = [
|
||||
},
|
||||
{
|
||||
name: 'Keycloak',
|
||||
href: '/guides/auth/auth-keycloak',
|
||||
href: '/guides/auth/social-login/auth-keycloak',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -100,7 +82,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'LinkedIn',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-linkedin',
|
||||
href: '/guides/auth/social-login/auth-linkedin',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -109,7 +91,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'MessageBird',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-messagebird',
|
||||
href: '/guides/auth/phone-login/messagebird',
|
||||
official: false,
|
||||
supporter: 'MessageBird',
|
||||
platform: true,
|
||||
@@ -118,7 +100,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Notion',
|
||||
// logo: '/img/libraries/notion-icon.svg',
|
||||
href: '/guides/auth/auth-notion',
|
||||
href: '/guides/auth/social-login/auth-notion',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -127,7 +109,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Slack',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-slack',
|
||||
href: '/guides/auth/social-login/auth-slack',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -136,7 +118,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Spotify',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-spotify',
|
||||
href: '/guides/auth/social-login/auth-spotify',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -145,7 +127,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Twitter',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-twitter',
|
||||
href: '/guides/auth/social-login/auth-twitter',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -154,7 +136,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Twitch',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-twitch',
|
||||
href: '/guides/auth/social-login/auth-twitch',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -163,7 +145,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Zoom',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-zoom',
|
||||
href: '/guides/auth/social-login/auth-zoom',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -172,7 +154,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Twilio',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-twilio',
|
||||
href: '/guides/auth/phone-login/twilio',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -180,7 +162,7 @@ const authProviders = [
|
||||
},
|
||||
{
|
||||
name: 'Vonage',
|
||||
href: '/guides/auth/auth-vonage',
|
||||
href: '/guides/auth/phone-login/vonage',
|
||||
official: false,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
|
||||
@@ -1,150 +1,10 @@
|
||||
[
|
||||
{ "title": "Contributing", "url": "/handbook/contributing" },
|
||||
{ "title": "Changelog", "url": "https://supabase.com/changelog" },
|
||||
{
|
||||
"title": "Product",
|
||||
"links": [
|
||||
{
|
||||
"text": "Database",
|
||||
"url": "/database"
|
||||
},
|
||||
{
|
||||
"text": "Auth",
|
||||
"url": "/auth"
|
||||
},
|
||||
{
|
||||
"text": "Functions",
|
||||
"url": "/edge-functions"
|
||||
},
|
||||
{
|
||||
"text": "Realtime",
|
||||
"url": "/realtime"
|
||||
},
|
||||
{
|
||||
"text": "Storage",
|
||||
"url": "/storage"
|
||||
},
|
||||
{
|
||||
"text": "Pricing",
|
||||
"url": "/pricing"
|
||||
},
|
||||
{
|
||||
"text": "Beta",
|
||||
"url": "/beta"
|
||||
}
|
||||
]
|
||||
"title": "Author Styleguide",
|
||||
"url": "https://github.com/supabase/supabase/blob/master/DEVELOPERS.md"
|
||||
},
|
||||
{
|
||||
"title": "Resources",
|
||||
"links": [
|
||||
{
|
||||
"text": "Support",
|
||||
"url": "/support"
|
||||
},
|
||||
{
|
||||
"text": "System Status",
|
||||
"url": "https://status.supabase.com/"
|
||||
},
|
||||
{
|
||||
"text": "Integrations",
|
||||
"url": "/partners/integrations"
|
||||
},
|
||||
{
|
||||
"text": "Experts",
|
||||
"url": "/partners/experts"
|
||||
},
|
||||
{
|
||||
"text": "Brand Assets / Logos",
|
||||
"url": "/brand-assets"
|
||||
},
|
||||
{
|
||||
"text": "DPA",
|
||||
"url": "/legal/dpa"
|
||||
},
|
||||
{
|
||||
"text": "SOC2",
|
||||
"url": "/legal/soc2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Developers",
|
||||
"links": [
|
||||
{
|
||||
"text": "Documentation",
|
||||
"url": "/docs"
|
||||
},
|
||||
{
|
||||
"text": "API Reference",
|
||||
"url": "/docs/reference"
|
||||
},
|
||||
{
|
||||
"text": "Changelog",
|
||||
"url": "/changelog"
|
||||
},
|
||||
{
|
||||
"text": "Contributing",
|
||||
"url": "/docs/handbook/contributing"
|
||||
},
|
||||
{
|
||||
"text": "Open Source",
|
||||
"url": "/docs/oss"
|
||||
},
|
||||
{
|
||||
"text": "SupaSquad",
|
||||
"url": "/supasquad"
|
||||
},
|
||||
{
|
||||
"text": "DevTo",
|
||||
"url": "https://dev.to/supabase"
|
||||
},
|
||||
{
|
||||
"text": "RSS",
|
||||
"url": "/rss.xml"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Company",
|
||||
"links": [
|
||||
{
|
||||
"text": "Blog",
|
||||
"url": "/blog"
|
||||
},
|
||||
{
|
||||
"text": "Careers",
|
||||
"url": "https://about.supabase.com/careers"
|
||||
},
|
||||
{
|
||||
"text": "Company",
|
||||
"url": "/company"
|
||||
},
|
||||
{
|
||||
"text": "Terms of Service",
|
||||
"url": "/docs/company/terms"
|
||||
},
|
||||
{
|
||||
"text": "Privacy Policy",
|
||||
"url": "/docs/company/privacy"
|
||||
},
|
||||
{
|
||||
"text": "Acceptable Use Policy",
|
||||
"url": "/docs/company/aup"
|
||||
},
|
||||
{
|
||||
"text": "Service Level Agreement",
|
||||
"url": "/docs/company/sla"
|
||||
},
|
||||
{
|
||||
"text": "Humans.txt",
|
||||
"url": "/humans.txt"
|
||||
},
|
||||
{
|
||||
"text": "Lawyers.txt",
|
||||
"url": "/lawyers.txt"
|
||||
},
|
||||
{
|
||||
"text": "Security.txt",
|
||||
"url": "/.well-known/security.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
{ "title": "Open Source", "url": "https://supabase.com/docs/oss" },
|
||||
{ "title": "SupaSquad", "url": "https://supabase.com/supasquad" }
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
import sections from '../../../../spec/common-api-sections.json' assert { type: 'json' }
|
||||
import { flattenSections } from '../helpers.mjs'
|
||||
|
||||
const flatSections = flattenSections(sections)
|
||||
|
||||
export function generateAPIPages() {
|
||||
let apiPages = []
|
||||
|
||||
flatSections.map((section) => {
|
||||
apiPages.push(`reference/api/${section.slug}`)
|
||||
})
|
||||
return apiPages
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import fs from 'fs'
|
||||
import yaml from 'js-yaml'
|
||||
import cliCommonSections from '../../../../spec/common-cli-sections.json' assert { type: 'json' }
|
||||
import { flattenSections } from '../helpers.mjs'
|
||||
|
||||
const flatCLISections = flattenSections(cliCommonSections)
|
||||
|
||||
const cliSpec = yaml.load(fs.readFileSync(`../../spec/cli_v1_commands.yaml`, 'utf8'))
|
||||
|
||||
export function generateCLIPages() {
|
||||
let cliPages = []
|
||||
|
||||
cliSpec.commands.map((section) => {
|
||||
const slug = flatCLISections.find((item) => item.id === section.id)?.slug
|
||||
if (slug) cliPages.push(`reference/cli/${slug}`)
|
||||
})
|
||||
return cliPages
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import yaml from 'js-yaml'
|
||||
import commonLibSections from '../../../../spec/common-client-libs-sections.json' assert { type: 'json' }
|
||||
import { flattenSections } from '../helpers.mjs'
|
||||
|
||||
const flatCommonLibSections = flattenSections(commonLibSections)
|
||||
|
||||
const clientLibFiles = [
|
||||
{ fileName: 'supabase_js_v2', label: 'javascript', version: 'v2', versionSlug: false },
|
||||
{ fileName: 'supabase_js_v1', label: 'javascript', version: 'v1', versionSlug: true },
|
||||
{ fileName: 'supabase_dart_v1', label: 'dart', version: 'v1', versionSlug: false },
|
||||
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
|
||||
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
|
||||
]
|
||||
|
||||
export function generateReferencePages() {
|
||||
let refPages = []
|
||||
clientLibFiles.map((file) => {
|
||||
const spec = yaml.load(fs.readFileSync(`../../spec/${file.fileName}.yml`, 'utf8'))
|
||||
spec.functions.map((fn) => {
|
||||
const slug = flatCommonLibSections.find((item) => item.id === fn.id)?.slug
|
||||
refPages.push(`reference/${file.label}/${file.versionSlug ? file.version + '/' : ''}${slug}`)
|
||||
})
|
||||
})
|
||||
return refPages
|
||||
}
|
||||
@@ -1,65 +1,49 @@
|
||||
import { writeFileSync } from 'fs'
|
||||
import { globby } from 'globby'
|
||||
import prettier from 'prettier'
|
||||
|
||||
/*
|
||||
* kudos to leerob from vercel
|
||||
* https://leerob.io/blog/nextjs-sitemap-robots
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'fs'
|
||||
import { globby } from 'globby'
|
||||
import prettier from 'prettier'
|
||||
import { generateCLIPages } from './files/cli.mjs'
|
||||
import { generateReferencePages } from './files/reference-lib.mjs'
|
||||
import { generateAPIPages } from './files/api.mjs'
|
||||
|
||||
const referencePages = generateReferencePages()
|
||||
const cliPages = generateCLIPages()
|
||||
const apiPages = generateAPIPages()
|
||||
|
||||
async function generate() {
|
||||
const prettierConfig = await prettier.resolveConfig('./.prettierrc.js')
|
||||
|
||||
const rawPages = await globby([
|
||||
// guides
|
||||
'docs/*.mdx',
|
||||
'!docs/404.mdx',
|
||||
'pages/**/*.mdx',
|
||||
// reference
|
||||
'docs/reference/*.mdx',
|
||||
'docs/reference/javascript/*.mdx',
|
||||
'docs/reference/javascript/generated/*.mdx',
|
||||
'!docs/reference/javascript/v1.mdx', // ignore this
|
||||
'docs/reference/dart/*.mdx',
|
||||
'docs/reference/dart/generated/*.mdx',
|
||||
'!docs/reference/dart/v0.mdx', // ignore this
|
||||
'docs/reference/api/*.mdx',
|
||||
'docs/reference/api/generated/*.mdx',
|
||||
'docs/reference/cli/*.mdx',
|
||||
'docs/reference/cli/generated/*.mdx',
|
||||
// misc reference
|
||||
'docs/reference/postgres/*.mdx',
|
||||
'docs/reference/postgres/generated/*.mdx',
|
||||
'docs/reference/realtime/*.mdx',
|
||||
'docs/reference/realtime/generated/*.mdx',
|
||||
'docs/reference/storage/*.mdx',
|
||||
'docs/reference/storage/generated/*.mdx',
|
||||
'docs/reference/auth/*.mdx',
|
||||
'docs/reference/auth/generated/*.mdx',
|
||||
'!pages/404.mdx',
|
||||
'!pages/404.tsx',
|
||||
'!pages/ref-pages.mdx',
|
||||
])
|
||||
|
||||
const pages = rawPages.map((x) => {
|
||||
const guidePages = rawPages.map((x) => {
|
||||
let string = x
|
||||
string = string.replace('/generated', '')
|
||||
string = string.replace('pages/', '')
|
||||
string = string.replace('.mdx', '')
|
||||
return string
|
||||
})
|
||||
|
||||
// add static OSS page
|
||||
pages.unshift('docs/oss')
|
||||
// add static homepage
|
||||
pages.unshift('docs')
|
||||
// combine the guidePages with the ref pages
|
||||
const allPages = guidePages.concat(referencePages, cliPages, apiPages)
|
||||
|
||||
const sitemap = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${pages
|
||||
${allPages
|
||||
.map((path) => {
|
||||
// fix homepage
|
||||
path = path.replace('pages/index', '')
|
||||
return `
|
||||
<url>
|
||||
<loc>${`https://supabase.com/${path.replace('pages/', 'docs/')}`}</loc>
|
||||
<loc>${`https://supabase.com/docs/${path}`}</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -75,7 +59,9 @@ async function generate() {
|
||||
})
|
||||
|
||||
const sitemapFilePath = `public/sitemap.xml`
|
||||
console.log(`Total of ${pages.length} pages in sitemap, located at /apps/docs/${sitemapFilePath}`)
|
||||
console.log(
|
||||
`Total of ${allPages.length} pages in sitemap, located at /apps/docs/${sitemapFilePath}`
|
||||
)
|
||||
|
||||
// eslint-disable-next-line no-sync
|
||||
writeFileSync(sitemapFilePath, formatted)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export function flattenSections(sections) {
|
||||
var a = []
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
if (sections[i].id) {
|
||||
// only push a section that has an id
|
||||
// these are reserved for sidebar subtitles
|
||||
a.push(sections[i])
|
||||
}
|
||||
if (sections[i].items) {
|
||||
// if there are subitems, loop through
|
||||
a = a.concat(flattenSections(sections[i].items))
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// generator files need their own tsconfig.json because they don't like "module": "esnext" setting that nextjs requires in the main tsconfig
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"noImplicitAny": false,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useEffect, FC } from 'react'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import NavBar from '../components/Navigation/NavBar'
|
||||
import SideBar from '../components/Navigation/SideBar'
|
||||
import Footer from '../components/Footer'
|
||||
import TableOfContents from '~/components/TableOfContents'
|
||||
import { menuItems } from '~/components/Navigation/Navigation.constants'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getPageType } from '~/lib/helpers'
|
||||
|
||||
interface Props {
|
||||
meta: { title: string; description?: string; hide_table_of_contents?: boolean }
|
||||
children: any
|
||||
toc?: any
|
||||
menuItems?: any
|
||||
currentPage?: string
|
||||
}
|
||||
|
||||
const Layout: FC<Props> = ({ meta, children, toc }) => {
|
||||
const { asPath } = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const key = localStorage.getItem('supabaseDarkMode')
|
||||
if (!key) {
|
||||
// Default to dark mode if no preference config
|
||||
document.documentElement.className = 'dark'
|
||||
} else {
|
||||
document.documentElement.className = key === 'true' ? 'dark' : ''
|
||||
}
|
||||
}, [])
|
||||
|
||||
const hasTableOfContents =
|
||||
toc !== undefined && toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0
|
||||
|
||||
const pageType = getPageType(asPath)
|
||||
|
||||
console.log('Default, pageType', pageType)
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${meta?.title} | Supabase`}
|
||||
description={meta?.description ? meta?.description : meta?.title}
|
||||
openGraph={{
|
||||
title: meta?.title,
|
||||
description: meta?.description,
|
||||
url: `https://supabase.com/docs/${asPath}`,
|
||||
images: [
|
||||
{
|
||||
url: `https://supabase.com/docs/img/supabase-og-image.png`,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
|
||||
<main>
|
||||
<NavBar />
|
||||
<div className="flex w-full flex-row">
|
||||
<SideBar menuItems={menuItems[pageType]} />
|
||||
<div className="main-content-pane docs-width grid md:grid-cols-12 gap-4 justify-between p-4 pb-8 w-full">
|
||||
<div
|
||||
className={`${
|
||||
meta?.hide_table_of_contents || !hasTableOfContents
|
||||
? 'col-span-12 xl:col-start-2 xl:col-span-10 2xl:col-start-3 2xl:col-span-8'
|
||||
: 'col-span-12 lg:col-span-9'
|
||||
} py-2 lg:py-4 px-2 lg:px-8 mx-auto`}
|
||||
>
|
||||
<article
|
||||
className={`${
|
||||
meta?.hide_table_of_contents || !hasTableOfContents ? 'xl:min-w-[880px]' : ''
|
||||
} doc-content-container prose dark:prose-dark dark:bg-scale-200 width-full mt-8 2xl:max-w-[880px]`}
|
||||
>
|
||||
{children}
|
||||
</article>
|
||||
</div>
|
||||
{hasTableOfContents && !meta?.hide_table_of_contents && (
|
||||
<div
|
||||
className={[
|
||||
'border-scale-400 dark:bg-scale-200 table-of-contents-height border-l',
|
||||
'thin-scrollbar overflow-y-auto sticky hidden xl:block md:col-span-3 px-2',
|
||||
].join(' ')}
|
||||
>
|
||||
<TableOfContents toc={toc} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
@@ -9,8 +9,9 @@ import FooterHelpCallout from '~/components/FooterHelpCallout'
|
||||
|
||||
import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState'
|
||||
import { useEffect } from 'react'
|
||||
import Footer from '~/components/Navigation/Footer'
|
||||
|
||||
const SiteRefLayout = ({ children }) => {
|
||||
const SiteLayout = ({ children }) => {
|
||||
const { isDarkMode } = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -63,6 +64,10 @@ const SiteRefLayout = ({ children }) => {
|
||||
icon: '/docs/img/icons/menu/resources',
|
||||
name: 'Resources',
|
||||
},
|
||||
self_hosting: {
|
||||
icon: '/docs/img/icons/menu/self-hosting',
|
||||
name: 'Self-Hosting',
|
||||
},
|
||||
integrations: {
|
||||
icon: '/docs/img/icons/menu/integrations',
|
||||
name: 'Integrations',
|
||||
@@ -270,31 +275,8 @@ const SiteRefLayout = ({ children }) => {
|
||||
</div>
|
||||
<div className="grow px-5 max-w-7xl mx-auto py-16">
|
||||
{children}
|
||||
|
||||
<div className="mt-32">
|
||||
<FooterHelpCallout />
|
||||
</div>
|
||||
<hr className="border-scale-400 mt-8"></hr>
|
||||
<div className="flex flex-col lg:flex-row gap-3 mt-6">
|
||||
<span className="text-xs text-scale-900">Supabase 2022</span>
|
||||
<span className="text-xs text-scale-900">—</span>
|
||||
<Link href="/handbook/contributing">
|
||||
<a className="text-xs text-scale-800 hover:underline">Contributing</a>
|
||||
</Link>
|
||||
<Link href="https://supabase.com/changelog">
|
||||
<a className="text-xs text-scale-800 hover:underline">Changelog</a>
|
||||
</Link>
|
||||
|
||||
<Link href="https://github.com/supabase/supabase/blob/master/DEVELOPERS.md">
|
||||
<a className="text-xs text-scale-800 hover:underline">Author Styleguide</a>
|
||||
</Link>
|
||||
<Link href="https://supabase.com/docs/oss">
|
||||
<a className="text-xs text-scale-800 hover:underline">Open Source</a>
|
||||
</Link>
|
||||
<Link href="https://supabase.com/supasquad">
|
||||
<a className="text-xs text-scale-800 hover:underline">SupaSquad</a>
|
||||
</Link>
|
||||
</div>
|
||||
<FooterHelpCallout />
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -317,4 +299,4 @@ const SiteRefLayout = ({ children }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default SiteRefLayout
|
||||
export default SiteLayout
|
||||
@@ -1,321 +0,0 @@
|
||||
import { useTheme } from 'common/Providers'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import NavigationMenu from '~/components/Navigation/NavigationMenu/NavigationMenu'
|
||||
import TopNavBarRef from '~/components/Navigation/NavigationMenu/TopNavBarRef'
|
||||
|
||||
import FooterHelpCallout from '~/components/FooterHelpCallout'
|
||||
|
||||
import { menuState } from '~/hooks/useMenuState'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const SiteRefLayout = ({ children }) => {
|
||||
const { isDarkMode } = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
const key = localStorage.getItem('supabaseDarkMode')
|
||||
if (!key) {
|
||||
// Default to dark mode if no preference config
|
||||
document.documentElement.className = 'dark'
|
||||
} else {
|
||||
document.documentElement.className = key === 'true' ? 'dark' : ''
|
||||
}
|
||||
}, [])
|
||||
|
||||
const menuLevel = 'home'
|
||||
// useMenuLevelId()
|
||||
const mobileMenuOpen = false
|
||||
|
||||
const levelsData = {
|
||||
home: {
|
||||
icon: '/docs/img/icons/menu/home',
|
||||
name: 'Home',
|
||||
},
|
||||
gettingstarted: {
|
||||
icon: '/docs/img/icons/menu/getting-started',
|
||||
name: 'Getting Started',
|
||||
},
|
||||
database: {
|
||||
icon: '/docs/img/icons/menu/database',
|
||||
name: 'Database',
|
||||
},
|
||||
auth: {
|
||||
icon: '/docs/img/icons/menu/auth',
|
||||
name: 'Auth',
|
||||
},
|
||||
functions: {
|
||||
icon: '/docs/img/icons/menu/functions',
|
||||
name: 'Functions',
|
||||
},
|
||||
realtime: {
|
||||
icon: '/docs/img/icons/menu/realtime',
|
||||
name: 'Realtime',
|
||||
},
|
||||
storage: {
|
||||
icon: '/docs/img/icons/menu/storage',
|
||||
name: 'Storage',
|
||||
},
|
||||
platform: {
|
||||
icon: '/docs/img/icons/menu/platform',
|
||||
name: 'Platform',
|
||||
},
|
||||
resources: {
|
||||
icon: '/docs/img/icons/menu/resources',
|
||||
name: 'Resources',
|
||||
},
|
||||
integrations: {
|
||||
icon: '/docs/img/icons/menu/integrations',
|
||||
name: 'Integrations',
|
||||
},
|
||||
reference_javascript_v1: {
|
||||
icon: '/docs/img/icons/menu/reference-javascript',
|
||||
name: 'Javascript Reference v1.0',
|
||||
},
|
||||
reference_javascript_v2: {
|
||||
icon: '/docs/img/icons/menu/reference-javascript',
|
||||
name: 'Javascript Reference v2.0',
|
||||
},
|
||||
reference_dart_v0: {
|
||||
icon: '/docs/img/icons/menu/reference-dart',
|
||||
name: 'Dart Reference v0.0',
|
||||
},
|
||||
reference_dart_v1: {
|
||||
icon: '/docs/img/icons/menu/reference-dart',
|
||||
name: 'Dart Reference v0.0',
|
||||
},
|
||||
reference_cli: {
|
||||
icon: '/docs/img/icons/menu/reference-cli',
|
||||
name: 'CLI Reference',
|
||||
},
|
||||
reference_api: {
|
||||
icon: '/docs/img/icons/menu/reference-api',
|
||||
name: 'Management API Reference',
|
||||
},
|
||||
reference_self_hosting_auth: {
|
||||
icon: '/docs/img/icons/menu/reference-auth',
|
||||
name: 'Auth Server Reference',
|
||||
},
|
||||
reference_self_hosting_storage: {
|
||||
icon: '/docs/img/icons/menu/reference-storage',
|
||||
name: 'Storage Server Reference',
|
||||
},
|
||||
reference_self_hosting_realtime: {
|
||||
icon: '/docs/img/icons/menu/reference-realtime',
|
||||
name: 'Realtime Server Reference',
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
{/* <img
|
||||
src={`${router.basePath}/img/gradient-bg.png`}
|
||||
className="absolute left-0 w-[520px] top-0 -z-1 pointer-events-none"
|
||||
/> */}
|
||||
|
||||
<div className="flex flex-row h-screen">
|
||||
<div
|
||||
className={[
|
||||
'hidden', // experiment hidden
|
||||
'transition-all',
|
||||
'top-0',
|
||||
'relative',
|
||||
'h-screen w-[420px]',
|
||||
'z-10',
|
||||
'lg:flex flex-col ml-0',
|
||||
].join(' ')}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
'top-0',
|
||||
'relative',
|
||||
'w-auto',
|
||||
'z-10',
|
||||
|
||||
'border-r overflow-auto h-screen',
|
||||
'backdrop-blur backdrop-filter bg-white-1200 dark:bg-blackA-300',
|
||||
'flex flex-col',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="top-0 sticky z-10">
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
className={[
|
||||
'hidden md:flex md:height-auto',
|
||||
'pt-8 bg-scale-200 flex-col gap-8',
|
||||
].join(' ')}
|
||||
>
|
||||
<Link href="/">
|
||||
<a className="px-10 flex items-center gap-2">
|
||||
<Image
|
||||
className="cursor-pointer"
|
||||
src={isDarkMode ? '/docs/supabase-dark.svg' : '/docs/supabase-light.svg'}
|
||||
width={96}
|
||||
height={24}
|
||||
alt="Supabase Logo"
|
||||
/>
|
||||
<span className="font-mono text-sm font-medium text-brand-900">DOCS</span>
|
||||
</a>
|
||||
</Link>
|
||||
{/* {router.asPath.includes('/reference/') && <RefSwitcher />} */}
|
||||
</div>
|
||||
<div className="h-4 bg-scale-200 w-full"></div>
|
||||
<div className="bg-gradient-to-b from-scale-200 to-transparent h-4 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={['pb-10 px-5 md:px-10 md:flex'].join(' ')}>
|
||||
<NavigationMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
// #docs-content-container is used by layout to scroll to top
|
||||
id="docs-content-container"
|
||||
className={[
|
||||
'w-full h-screen transition-all ease-out',
|
||||
mobileMenuOpen ? 'overflow-hidden' : 'overflow-auto',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex flex-col relative">
|
||||
{/* <NavigationMenu /> */}
|
||||
<div className={['lg:sticky top-0 z-10 overflow-hidden'].join(' ')}>
|
||||
<TopNavBarRef />
|
||||
</div>
|
||||
<div
|
||||
className={[
|
||||
'sticky transition-all top-0',
|
||||
'z-10',
|
||||
'backdrop-blur backdrop-filter bg-white-1200 dark:bg-blackA-300',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className={['lg:hidden', 'px-5 ', 'border-b z-10'].join(' ')}>
|
||||
<div
|
||||
className={[
|
||||
'transition-all ease-out z-10',
|
||||
'top-0',
|
||||
mobileMenuOpen && 'absolute',
|
||||
'flex items-center h-[40px]',
|
||||
mobileMenuOpen ? 'gap-0' : 'gap-3',
|
||||
].join(' ')}
|
||||
>
|
||||
<button
|
||||
className={['mr-2', mobileMenuOpen && 'mt-0.5'].join(' ')}
|
||||
onClick={() => menuState.setMenuMobileOpen(!mobileMenuOpen)}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
'space-y-1 group cursor-pointer relative',
|
||||
mobileMenuOpen ? 'w-4 h-4' : 'w-4 h-[8px]',
|
||||
].join(' ')}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
'transition-all ease-out block w-4 h-px bg-scale-900 group-hover:bg-scale-1200',
|
||||
!mobileMenuOpen ? 'w-4' : 'absolute rotate-45 top-[6px]',
|
||||
].join(' ')}
|
||||
></span>
|
||||
<span
|
||||
className={[
|
||||
'transition-all ease-out block h-px bg-scale-900 group-hover:bg-scale-1200',
|
||||
!mobileMenuOpen
|
||||
? 'w-3 group-hover:w-4'
|
||||
: 'absolute w-4 -rotate-45 top-[2px]',
|
||||
].join(' ')}
|
||||
></span>
|
||||
</div>
|
||||
</button>
|
||||
<div className={[].join(' ')}>
|
||||
<img
|
||||
src={
|
||||
menuLevel
|
||||
? levelsData[menuLevel].icon + '.svg'
|
||||
: levelsData['home'].icon + '.svg'
|
||||
}
|
||||
className={[
|
||||
'transition-all duration-200',
|
||||
mobileMenuOpen ? 'invisible w-0 h-0' : 'w-4 h-4',
|
||||
].join(' ')}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={[
|
||||
'transition-all duration-200',
|
||||
'text-scale-1200',
|
||||
mobileMenuOpen ? 'text-xs' : 'text-sm',
|
||||
].join(' ')}
|
||||
>
|
||||
{mobileMenuOpen
|
||||
? 'Close'
|
||||
: menuLevel
|
||||
? levelsData[menuLevel].name
|
||||
: levelsData['home'].name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={[
|
||||
'transition-all ease-out duration-200',
|
||||
'absolute left-0 right-0 h-screen',
|
||||
'px-5 pl-5 py-16',
|
||||
'top-[0px]',
|
||||
'bg-scale-200',
|
||||
mobileMenuOpen
|
||||
? 'overflow-y-auto opacity-100 left-[0px] visible'
|
||||
: 'left-[-40px] h-0 opacity-0 invisible',
|
||||
].join(' ')}
|
||||
>
|
||||
<NavigationMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow px-5 max-w-7xl mx-auto py-16">
|
||||
{children}
|
||||
|
||||
<div className="mt-32">
|
||||
<FooterHelpCallout />
|
||||
</div>
|
||||
<hr className="border-scale-400 mt-8"></hr>
|
||||
<div className="flex flex-col lg:flex-row gap-3 mt-6">
|
||||
<span className="text-xs text-scale-900">Supabase 2022</span>
|
||||
<span className="text-xs text-scale-900">—</span>
|
||||
<Link href="/handbook/contributing">
|
||||
<a className="text-xs text-scale-800 hover:underline">Contributing</a>
|
||||
</Link>
|
||||
<Link href="https://supabase.com/changelog">
|
||||
<a className="text-xs text-scale-800 hover:underline">Changelog</a>
|
||||
</Link>
|
||||
|
||||
<Link href="https://github.com/supabase/supabase/blob/master/DEVELOPERS.md">
|
||||
<a className="text-xs text-scale-800 hover:underline">Author Styleguide</a>
|
||||
</Link>
|
||||
<Link href="https://supabase.com/docs/oss">
|
||||
<a className="text-xs text-scale-800 hover:underline">Open Source</a>
|
||||
</Link>
|
||||
<Link href="https://supabase.com/supasquad">
|
||||
<a className="text-xs text-scale-800 hover:underline">SupaSquad</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={[
|
||||
// 'top-0 left-0',
|
||||
'hidden', // experiment hidden
|
||||
'h-full',
|
||||
'left-0',
|
||||
'right-0',
|
||||
'z-10',
|
||||
'backdrop-blur-sm backdrop-filter bg-white-1200 dark:bg-blackA-600',
|
||||
mobileMenuOpen ? 'absolute MOBILE-MENU-OPEN' : 'hidden h-0 MOBILE-MENU-CLOSED',
|
||||
].join(' ')}
|
||||
onClick={() => menuState.setMenuMobileOpen(!mobileMenuOpen)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default SiteRefLayout
|
||||
@@ -76,7 +76,7 @@ const Layout: FC<Props> = (props) => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={['grid grid-cols-12 relative'].join(' ')}>
|
||||
<div className={['grid grid-cols-12 relative gap-4'].join(' ')}>
|
||||
<div
|
||||
className={[
|
||||
'relative',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { highlightSelectedNavItem } from '~/components/CustomHTMLElements/Custom
|
||||
import { useRouter } from 'next/router'
|
||||
import { useNavigationMenuContext } from '~/components/Navigation/NavigationMenu/NavigationMenu.Context'
|
||||
import { menuState } from '~/hooks/useMenuState'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface ISectionContainer {
|
||||
id: string
|
||||
@@ -77,8 +78,13 @@ const Section: FC<ISectionContainer> = (props) => {
|
||||
|
||||
const StickyHeader: FC<StickyHeader> = ({ icon, ...props }) => {
|
||||
const router = useRouter()
|
||||
|
||||
const { setActiveRefItem } = useNavigationMenuContext()
|
||||
|
||||
// we're serving search bots a different file (/crawlers/[...slug])
|
||||
// and need to modify content to suit that
|
||||
const isCrawlerPage = router.route.includes('/crawlers/[...slug]')
|
||||
|
||||
const { ref } = useInView({
|
||||
threshold: 1,
|
||||
rootMargin: '30% 0% -35% 0px',
|
||||
@@ -99,21 +105,25 @@ const StickyHeader: FC<StickyHeader> = ({ icon, ...props }) => {
|
||||
<div className={['flex items-center gap-3 not-prose', icon && 'mb-8'].join(' ')}>
|
||||
{icon && (
|
||||
<div className="w-8 h-8 bg-brand-500 rounded flex items-center justify-center">
|
||||
<img className="w-6 h-6" src={`${icon}.svg`} />
|
||||
<Image width={16} height={16} alt={icon} src={`${icon}.svg`} />
|
||||
</div>
|
||||
)}
|
||||
<h2
|
||||
ref={ref}
|
||||
id={props.slug}
|
||||
data-ref-id={props.id}
|
||||
className={[
|
||||
'text-2xl font-medium text-scale-1200 scroll-mt-24',
|
||||
!icon && 'mb-8',
|
||||
props.monoFont && 'font-mono',
|
||||
].join(' ')}
|
||||
>
|
||||
{props.title && <span className="max-w-xl">{props.title}</span>}
|
||||
</h2>
|
||||
{isCrawlerPage ? (
|
||||
<h1>{props.title}</h1>
|
||||
) : (
|
||||
<h2
|
||||
ref={ref}
|
||||
id={props.slug}
|
||||
data-ref-id={props.id}
|
||||
className={[
|
||||
'text-2xl font-medium text-scale-1200 scroll-mt-24',
|
||||
!icon && 'mb-8',
|
||||
props.monoFont && 'font-mono',
|
||||
].join(' ')}
|
||||
>
|
||||
{props.title && <span className="max-w-xl">{props.title}</span>}
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { MDXProvider } from '@mdx-js/react'
|
||||
import Head from 'next/head'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import components from '~/components'
|
||||
import Footer from '~/components/Footer'
|
||||
import SideBar from '~/components/Navigation/SideBar'
|
||||
import TableOfContents from '~/components/TableOfContents'
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// @ts-expect-error
|
||||
import specStorageV0 from '~/../../spec/storage_v0_config.yaml' assert { type: 'yml' }
|
||||
// @ts-expect-error
|
||||
import specRealtimeV0 from '~/../../spec/realtime_v0_config.yaml' assert { type: 'yml' }
|
||||
// @ts-expect-error
|
||||
import specAuthV1 from '~/../../spec/gotrue_v1_config.yaml' assert { type: 'yml' }
|
||||
|
||||
function getStorageConfigV0() {
|
||||
return { ...specStorageV0 }
|
||||
}
|
||||
|
||||
function getRealtimeConfigV0() {
|
||||
return { ...specRealtimeV0 }
|
||||
}
|
||||
|
||||
function getAuthConfigV1() {
|
||||
return { ...specAuthV1 }
|
||||
}
|
||||
|
||||
export { getStorageConfigV0, getRealtimeConfigV0, getAuthConfigV1 }
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TsDoc } from '~/generator/legacy/definitions'
|
||||
import { TsDoc } from '../../generator/legacy/definitions'
|
||||
|
||||
import { values, mapValues } from 'lodash'
|
||||
import { OpenAPIV3 } from 'openapi-types'
|
||||
@@ -60,6 +60,8 @@ function recurseThroughParams(paramDefinition: any) {
|
||||
children = dereferenced.children
|
||||
} else if (dereferenced.type?.declaration?.children) {
|
||||
children = dereferenced.type.declaration.children
|
||||
} else if (dereferenced.type?.type === 'query') {
|
||||
// skip: ignore types created from `typeof` for now, like `type Fetch = typeof fetch`
|
||||
} else if (dereferenced.type?.type === 'union') {
|
||||
// skip: we don't want to show unions as nested parameters
|
||||
} else if (Object.keys(dereferenced).length === 0) {
|
||||
|
||||
@@ -48,7 +48,12 @@ const nextConfig = {
|
||||
basePath: '/docs',
|
||||
images: {
|
||||
dangerouslyAllowSVG: true,
|
||||
domains: ['avatars.githubusercontent.com', 'github.com', 'user-images.githubusercontent.com'],
|
||||
domains: [
|
||||
'avatars.githubusercontent.com',
|
||||
'github.com',
|
||||
'user-images.githubusercontent.com',
|
||||
'raw.githubusercontent.com',
|
||||
],
|
||||
},
|
||||
experimental: {
|
||||
mdxRs: true,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"build:sitemap": "node ./internals/generate-sitemap.mjs",
|
||||
"postbuild": "node ./scripts/build-search.js && node ./internals/generate-sitemap.mjs",
|
||||
"postbuild": "ts-node ./scripts/build-search.ts && node ./internals/generate-sitemap.mjs",
|
||||
"generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-js:v1 gen:supabase-js:v2 gen:realtime",
|
||||
"gen:api": "npm-run-all gen:api:usage",
|
||||
"gen:api:usage": "ts-node ./generator/index.ts gen --type api --url https://api.supabase.com --input ../../spec/transforms/api_v0_openapi_deparsed.json --output ./docs/reference/api/generated/usage.mdx",
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import toc from 'markdown-toc'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import components from '../components/index'
|
||||
import NewLayout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
import Layout from '~/layouts/Default'
|
||||
import { getAllDocs, getDocsBySlug } from '../lib/docs'
|
||||
|
||||
interface Meta {
|
||||
id: string
|
||||
title: string
|
||||
sidebar_label: string
|
||||
hide_table_of_contents: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
meta: Meta
|
||||
content: any
|
||||
toc: any
|
||||
}
|
||||
const isNewDocs = process.env.NEXT_PUBLIC_NEW_DOCS === 'true'
|
||||
|
||||
export default function Doc({ meta, content, toc }: Props) {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<>
|
||||
{Object.entries(meta).length > 0 || !isNewDocs ? (
|
||||
<Layout meta={meta} toc={toc}>
|
||||
<MDXRemote {...content} components={components} />
|
||||
</Layout>
|
||||
) : (
|
||||
<NewLayout meta={meta} toc={toc}>
|
||||
<MDXRemote {...content} components={components} />
|
||||
</NewLayout>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params }: { params: { slug: string[] } }) {
|
||||
let slug
|
||||
if (params.slug.length > 1) {
|
||||
slug = `docs/${params.slug.join('/')}`
|
||||
} else {
|
||||
slug = `docs/${params.slug[0]}`
|
||||
}
|
||||
|
||||
let doc = getDocsBySlug(slug)
|
||||
const content = await serialize(doc.content || '')
|
||||
|
||||
return {
|
||||
props: {
|
||||
...doc,
|
||||
content,
|
||||
toc: toc(doc.content, { maxdepth: 1, firsth1: false }),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getStaticPaths() {
|
||||
let docs = getAllDocs()
|
||||
|
||||
return {
|
||||
paths: docs.map(() => {
|
||||
return {
|
||||
params: {
|
||||
slug: docs.map((d) => d.slug),
|
||||
},
|
||||
}
|
||||
}),
|
||||
fallback: 'blocking',
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ Self-hosting Supabase is free. If you wish to use our cloud-platform, we provide
|
||||
|
||||
### How do I host Supabase?
|
||||
|
||||
You can use the docker-compose script [here](https://github.com/supabase/supabase/tree/master/docker), and find detailed instructions [here](/docs/guides/hosting/overview).
|
||||
You can use the docker compose script [here](https://github.com/supabase/supabase/tree/master/docker), and find detailed instructions [here](/docs/guides/hosting/overview).
|
||||
|
||||
Supabase is an amalgamation of open source tools. Some of these tools are made by Supabase (like our [Realtime Server](https://github.com/supabase/realtime)), some we support directly (like [PostgREST](http://postgrest.org/en/v7.0.0/)), and some are third-party tools (like [KonSupabase is an amalgamation open sourceg](https://github.com/Kong/kong)).
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Email Templates',
|
||||
description: 'Learn how to configure the email templates on Supabase.',
|
||||
}
|
||||
|
||||
You can customize the email messages used for the authentication flows. You can edit the following email templates:
|
||||
|
||||
- Confirm signup
|
||||
- Invite user
|
||||
- Magic Link
|
||||
- Change Email Address
|
||||
- Reset Password
|
||||
|
||||
## Terminology
|
||||
|
||||
The templating system provides the following variables for use:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `{{ .ConfirmationURL }}` | Contains the confirmation URL. For example, a signup confirmation URL would look like: `https://project-ref.supabase.co/auth/v1/verify?token={{ .TokenHash }}&type=signup&redirect_to=https://example.com/path` . |
|
||||
| `{{ .Token }}` | Contains a 6-digit One-Time-Password (OTP) that can be used instead of the `{{. ConfirmationURL }}` . |
|
||||
| `{{ .TokenHash }}` | Contains a hashed version of the `{{ .Token }}`. This is useful for constructing your own email link in the email template. |
|
||||
| `{{ .SiteURL }}` | Contains your application's Site URL. This can be configured in your project's [authentication settings](https://app.supabase.com/project/_/auth/url-configuration). |
|
||||
|
||||
## Limitations
|
||||
|
||||
### Email Prefetching
|
||||
|
||||
Certain email providers may have spam detection or other security features that prefetch URL links from incoming emails.
|
||||
In this scenario, the `{{ .ConfirmationURL }}` sent will be consumed instantly which leads to a "Token has expired or is invalid" error.
|
||||
To guard against this:
|
||||
|
||||
- Use an email OTP instead by including `{{ .Token }}` in the email template.
|
||||
- Create your own custom email link to redirect the user to a page where they can click on a button to confirm the action.
|
||||
For example, you can include the following in your email template:
|
||||
|
||||
```html
|
||||
<a href="{{ .SiteURL }}/confirm-signup?confirmation_url={{ .ConfirmationURL }}"
|
||||
>Confirm your signup
|
||||
</a>
|
||||
```
|
||||
|
||||
The user should be brought to a page on your site where they can confirm the action by clicking a button.
|
||||
The button should contain the actual confirmation link which can be obtained from parsing the `confirmation_url={{ .ConfirmationURL }}` query parameter in the URL.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -95,7 +95,7 @@ Create a new file at `/utils/supabase-browser.ts` and populate with the followin
|
||||
|
||||
```ts
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { Database } from '../db_types'
|
||||
import { Database } from '../lib/database.types'
|
||||
|
||||
export default createBrowserSupabaseClient<Database>()
|
||||
```
|
||||
@@ -109,7 +109,7 @@ 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 { Database } from '../db_types'
|
||||
import { Database } from '../lib/database.types'
|
||||
|
||||
export default () =>
|
||||
createServerComponentSupabaseClient<Database>({
|
||||
@@ -139,6 +139,8 @@ Middleware runs before every route declared in the `matcher` array. Since we don
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Create a new file at `/pages/middleware.js` and populate with the following:
|
||||
|
||||
```jsx title="middleware.js"
|
||||
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
@@ -164,6 +166,8 @@ export const config = {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create a new file at `/middleware.ts` (a sibling of /pages) and populate with the following:
|
||||
|
||||
```tsx title="middleware.ts"
|
||||
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
@@ -254,7 +258,7 @@ export default async function RootLayout({ children }) {
|
||||
}
|
||||
```
|
||||
|
||||
We don't want Next.js to cache this `session` value, so we need to export a `revalidate` value of `0`.
|
||||
We don't want Next.js to cache this `session` value, so we need to export a `revalidate` value of `0` in the layout file.
|
||||
|
||||
```jsx
|
||||
export const revalidate = 0
|
||||
@@ -641,7 +645,7 @@ Create a new file at `/app/realtime/posts.tsx` and populate with the following:
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Database } from '../../db_types'
|
||||
import { Database } from '../lib/database.types'
|
||||
import supabase from '../../utils/supabase-browser'
|
||||
|
||||
type Post = Database['public']['Tables']['posts']['Row']
|
||||
|
||||
@@ -83,9 +83,9 @@ Wrap your `pages/_app.js` component with the `SessionContextProvider` component:
|
||||
```jsx title=pages/_app.js
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider } from '@supabase/auth-helpers-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const router = useRouter()
|
||||
// Create a new supabase browser client on every first render.
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
@@ -108,6 +108,7 @@ Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component
|
||||
```tsx lines=2,8 title=pages/_app.tsx
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
function MyApp({
|
||||
Component,
|
||||
@@ -162,6 +163,7 @@ const supabaseClient = useSupabaseClient<Database>()
|
||||
|
||||
```tsx
|
||||
// Creating a new supabase server client object (e.g. in API route):
|
||||
import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type { Database } from 'types_db'
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ For example, a `public.profiles` table might look like this:
|
||||
|
||||
```sql
|
||||
create table public.profiles (
|
||||
id uuid references auth.users not null,
|
||||
id uuid references auth.users not null on delete cascade,
|
||||
first_name text,
|
||||
last_name text,
|
||||
|
||||
@@ -28,6 +28,12 @@ create table public.profiles (
|
||||
alter table public.profiles enable row level security;
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Make sure to specify the `on delete cascade` clause when referencing `auth.users`. Omitting it may cause problems when deleting users.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Public access
|
||||
|
||||
Since Row Level Security is enabled, this table is accessible via the API but no data will be returned unless we set up some Policies.
|
||||
|
||||
@@ -36,11 +36,11 @@ It differs from the `http` extension in that it is asynchronous by default. This
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Example: enable the "pg_net" extension
|
||||
create schema if not exists net;
|
||||
create extension pg_net with schema net;
|
||||
-- Example: enable the "pg_net" extension.
|
||||
create extension pg_net;
|
||||
-- Note: The extension creates its own schema/namespace named "net" to avoid naming conflicts.
|
||||
|
||||
-- Example: disable the "plv8" extension
|
||||
-- Example: disable the "pg_net" extension
|
||||
drop extension if exists pg_net;
|
||||
drop schema net;
|
||||
```
|
||||
|
||||
@@ -10,8 +10,8 @@ The `uuid-ossp` extension can be used to generate a `UUID`.
|
||||
|
||||
## Overview
|
||||
|
||||
A `UUID` is a "Universally Unique Identifer" and it is, for practical purposes, unique.
|
||||
This makes them particularly well suited as Primary Keys. It is occasionally referred to as a `GUID`, which stands for "Globally Unique Identifer".
|
||||
A `UUID` is a "Universally Unique Identifier" and it is, for practical purposes, unique.
|
||||
This makes them particularly well suited as Primary Keys. It is occasionally referred to as a `GUID`, which stands for "Globally Unique Identifier".
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ For example if you had the following situations:
|
||||
|
||||
- You have a list of `movies`.
|
||||
- A movie can have several `actors`.
|
||||
- An `actor` can perfom in several movies.
|
||||
- An `actor` can perform in several movies.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
|
||||
@@ -38,7 +38,7 @@ export const meta = {
|
||||
|
||||
### Tutorials
|
||||
|
||||
<div className="grid grid-cols-12 gap-6 not-prose">
|
||||
<div className="grid lg:grid-cols-12 gap-6 not-prose">
|
||||
{webapps.map((item) => {
|
||||
return (
|
||||
<Link href={`/${item.href}`} key={item.title} passHref>
|
||||
@@ -150,13 +150,13 @@ export const resources = [
|
||||
{
|
||||
title: 'Features',
|
||||
hasLightIcon: true,
|
||||
href: '/features',
|
||||
href: '/guides/getting-started/features',
|
||||
description: 'A non-exhaustive list of features that Supabase provides for every project.',
|
||||
},
|
||||
{
|
||||
title: 'Architecture',
|
||||
hasLightIcon: true,
|
||||
href: '/architecture',
|
||||
href: '/guides/getting-started/architecture',
|
||||
description: "An overview of Supabase's architecture and product principles.",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -60,11 +60,11 @@ Build passwordless logins for your application or website.[Docs](/docs/guides/au
|
||||
|
||||
### Social Logins
|
||||
|
||||
Provide social logins - everything from Apple, to GitHub, to Slack. [Docs](/docs/guides/auth/auth-apple).
|
||||
Provide social logins - everything from Apple, to GitHub, to Slack. [Docs](/docs/guides/auth/social-login).
|
||||
|
||||
### Phone Logins
|
||||
|
||||
Provide phone logins using a 3rd-party SMS provider. [Docs](/docs/guides/auth/auth-twilio).
|
||||
Provide phone logins using a third-party SMS provider. [Docs](/docs/guides/auth/phone-login).
|
||||
|
||||
### Row Level Security
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ export class AuthComponent implements OnInit {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div
|
||||
</div>
|
||||
```
|
||||
|
||||
### Account page
|
||||
|
||||
@@ -50,7 +50,9 @@ Add `io.supabase.flutterquickstart://login-callback/` as a new [redirect URL](ht
|
||||
|
||||
That is it on Supabase's end and the rest are platform specific settings:
|
||||
|
||||
For Android, add an intent-filter to enable deep linking:
|
||||
For Android, edit the `android/app/src/main/AndroidManifest.xml` file.
|
||||
|
||||
Add an intent-filter to enable deep linking:
|
||||
|
||||
```xml title=android/app/src/main/AndroidManifest.xml
|
||||
<manifest ...>
|
||||
@@ -75,7 +77,9 @@ For Android, add an intent-filter to enable deep linking:
|
||||
</manifest>
|
||||
```
|
||||
|
||||
For iOS add CFBundleURLTypes to enable deep linking:
|
||||
For iOS, edit the ios/Runner/Info.plist file.
|
||||
|
||||
Add CFBundleURLTypes to enable deep linking:
|
||||
|
||||
```xml title=ios/Runner/Info.plist"
|
||||
<!-- ... other tags -->
|
||||
|
||||
@@ -37,7 +37,7 @@ Then let's install the only additional dependency: [supabase-js](https://github.
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in a `.env`.
|
||||
And finally we want to save the environment variables in a `.env.local` file.
|
||||
All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
```bash title=.env
|
||||
@@ -48,6 +48,8 @@ REACT_APP_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client. These variables will be exposed
|
||||
on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
|
||||
Create and edit `src/supabaseClient.js`:
|
||||
|
||||
```js title=src/supabaseClient.js
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
@@ -64,6 +66,8 @@ You can find the full contents of this file [here](https://raw.githubusercontent
|
||||
|
||||
Let's set up a React component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
|
||||
|
||||
Create and edit `src/Auth.js`:
|
||||
|
||||
```jsx title=src/Auth.js
|
||||
import { useState } from 'react'
|
||||
import { supabase } from './supabaseClient'
|
||||
@@ -120,7 +124,7 @@ export default function Auth() {
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
|
||||
Let's create a new component for that called `Account.js`.
|
||||
Let's create a new component for that called `src/Account.js`.
|
||||
|
||||
```jsx title=src/Account.js
|
||||
import { useState, useEffect } from 'react'
|
||||
@@ -234,7 +238,7 @@ export default Account
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `App.js`:
|
||||
Now that we have all the components in place, let's update `src/App.js`:
|
||||
|
||||
```jsx title=src/App.js
|
||||
import './index.css'
|
||||
@@ -282,6 +286,8 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
|
||||
|
||||
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:
|
||||
|
||||
Create and edit `src/Avatar.js`:
|
||||
|
||||
```jsx title=src/Avatar.js
|
||||
import { useEffect, useState } from 'react'
|
||||
import { supabase } from './supabaseClient'
|
||||
@@ -367,7 +373,7 @@ export default function Avatar({ url, size, onUpload }) {
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
And then we can add the widget to the Account page at `src/Account.js`:
|
||||
|
||||
```jsx title=src/Account.js
|
||||
// Import the new component
|
||||
@@ -376,7 +382,7 @@ import Avatar from './Avatar'
|
||||
// ...
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
<form onSubmit={updateProfile} className="form-widget">
|
||||
{/* Add to the body */}
|
||||
<Avatar
|
||||
url={avatar_url}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Layout from '~/layouts/DefaultLayout'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { GlassPanel } from 'ui'
|
||||
import { integrations } from '~/components/Navigation/NavigationMenu/NavigationMenu.constants'
|
||||
|
||||
@@ -20,7 +21,7 @@ Explore a variety of integrations from Supabase partners. Need a different integ
|
||||
<GlassPanel
|
||||
title={integration.name}
|
||||
background={false}
|
||||
icon={<img className="w-10 h-10 rounded" src={`/docs/img/integrations/logos/${integration.name.toLowerCase()}_logo.png`}/>}
|
||||
icon={<Image height={40} width={40} className="rounded" src={`/docs/img/integrations/logos/${integration.name.toLowerCase()}_logo.png`}/>}
|
||||
>
|
||||
{integration.description}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ cd supabase-authsignal-example
|
||||
|
||||
Create a `.env.local` file and enter the following values:
|
||||
|
||||
```
|
||||
```sh
|
||||
NEXT_PUBLIC_SUPABASE_URL=get-from-supabase-dashboard
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=get-from-supabase-dashboard
|
||||
AUTHSIGNAL_SECRET=get-from-authsignal-dashboard
|
||||
@@ -84,19 +84,19 @@ npm run dev
|
||||
|
||||
Install the Supabase client and Auth helpers for Next.js:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install @supabase/supabase-js @supabase/auth-helpers-nextjs
|
||||
```
|
||||
|
||||
Install the Authsignal Node.js client:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install @authsignal/node
|
||||
```
|
||||
|
||||
Finally install 2 packages to help encrypt and serialize session data in cookies:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install @hapi/iron cookie
|
||||
npm install --save-dev @types/cookie
|
||||
```
|
||||
@@ -105,18 +105,18 @@ npm install --save-dev @types/cookie
|
||||
|
||||
Add the following code to `/lib/authsignal.ts`:
|
||||
|
||||
```
|
||||
import { Authsignal } from "@authsignal/node";
|
||||
```ts
|
||||
import { Authsignal } from '@authsignal/node'
|
||||
|
||||
const secret = process.env.AUTHSIGNAL_SECRET;
|
||||
const secret = process.env.AUTHSIGNAL_SECRET
|
||||
|
||||
if (!secret) {
|
||||
throw new Error("AUTHSIGNAL_SECRET is undefined");
|
||||
throw new Error('AUTHSIGNAL_SECRET is undefined')
|
||||
}
|
||||
|
||||
const redirectUrl = "http://localhost:3000/api/callback";
|
||||
const redirectUrl = 'http://localhost:3000/api/callback'
|
||||
|
||||
export const authsignal = new Authsignal({ secret, redirectUrl });
|
||||
export const authsignal = new Authsignal({ secret, redirectUrl })
|
||||
```
|
||||
|
||||
The `redirectUrl` here is a Next.js API route which Authsignal will redirect back to after an MFA challenge. We'll implement this below.
|
||||
@@ -131,83 +131,72 @@ Next we will add some helper functions for managing cookies:
|
||||
|
||||
Add the following code to `/lib/cookies.ts`:
|
||||
|
||||
```
|
||||
import Iron from "@hapi/iron";
|
||||
import { Session } from "@supabase/supabase-js";
|
||||
import { parse, serialize } from "cookie";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
```ts
|
||||
import Iron from '@hapi/iron'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
import { parse, serialize } from 'cookie'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export async function setTempCookie(session: Session, res: NextApiResponse) {
|
||||
const token = await Iron.seal(session, TEMP_TOKEN_SECRET, Iron.defaults);
|
||||
const token = await Iron.seal(session, TEMP_TOKEN_SECRET, Iron.defaults)
|
||||
|
||||
const cookie = serialize(TEMP_COOKIE, token, {
|
||||
maxAge: session.expires_in,
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
});
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
})
|
||||
|
||||
res.setHeader("Set-Cookie", cookie);
|
||||
res.setHeader('Set-Cookie', cookie)
|
||||
}
|
||||
|
||||
export async function getSessionFromTempCookie(
|
||||
req: NextApiRequest
|
||||
): Promise<Session | undefined> {
|
||||
const cookie = req.headers.cookie as string;
|
||||
export async function getSessionFromTempCookie(req: NextApiRequest): Promise<Session | undefined> {
|
||||
const cookie = req.headers.cookie as string
|
||||
|
||||
const cookies = parse(cookie ?? "");
|
||||
const cookies = parse(cookie ?? '')
|
||||
|
||||
const tempCookie = cookies[TEMP_COOKIE];
|
||||
const tempCookie = cookies[TEMP_COOKIE]
|
||||
|
||||
if (!tempCookie) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
const session = await Iron.unseal(
|
||||
tempCookie,
|
||||
TEMP_TOKEN_SECRET,
|
||||
Iron.defaults
|
||||
);
|
||||
const session = await Iron.unseal(tempCookie, TEMP_TOKEN_SECRET, Iron.defaults)
|
||||
|
||||
return session;
|
||||
return session
|
||||
}
|
||||
|
||||
export function setAuthCookie(session: Session, res: NextApiResponse) {
|
||||
const { access_token, refresh_token, expires_in } = session;
|
||||
const { access_token, refresh_token, expires_in } = session
|
||||
|
||||
const authCookies = [
|
||||
{ name: ACCESS_TOKEN_COOKIE, value: access_token },
|
||||
refresh_token
|
||||
? { name: REFRESH_TOKEN_COOKIE, value: refresh_token }
|
||||
: undefined,
|
||||
refresh_token ? { name: REFRESH_TOKEN_COOKIE, value: refresh_token } : undefined,
|
||||
]
|
||||
.filter(isDefined)
|
||||
.map(({ name, value }) =>
|
||||
serialize(name, value, {
|
||||
maxAge: expires_in,
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
// Also clear the temp cookie
|
||||
const updatedCookies = [
|
||||
...authCookies,
|
||||
serialize(TEMP_COOKIE, "", { maxAge: -1, path: "/" }),
|
||||
];
|
||||
const updatedCookies = [...authCookies, serialize(TEMP_COOKIE, '', { maxAge: -1, path: '/' })]
|
||||
|
||||
res.setHeader("Set-Cookie", updatedCookies);
|
||||
res.setHeader('Set-Cookie', updatedCookies)
|
||||
}
|
||||
|
||||
const isDefined = <T>(value: T | undefined): value is T => !!value;
|
||||
const isDefined = <T>(value: T | undefined): value is T => !!value
|
||||
|
||||
const TEMP_TOKEN_SECRET = process.env.TEMP_TOKEN_SECRET!;
|
||||
const TEMP_COOKIE = "as-mfa-cookie";
|
||||
const ACCESS_TOKEN_COOKIE = "sb-access-token";
|
||||
const REFRESH_TOKEN_COOKIE = "sb-refresh-token";
|
||||
const TEMP_TOKEN_SECRET = process.env.TEMP_TOKEN_SECRET!
|
||||
const TEMP_COOKIE = 'as-mfa-cookie'
|
||||
const ACCESS_TOKEN_COOKIE = 'sb-access-token'
|
||||
const REFRESH_TOKEN_COOKIE = 'sb-refresh-token'
|
||||
```
|
||||
|
||||
## Step 7: Building the UI
|
||||
@@ -216,34 +205,34 @@ We will add some form components for signing in and signing up as well as a basi
|
||||
|
||||
Add the following code to `/pages/sign-up.tsx`:
|
||||
|
||||
```
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
```ts
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function SignUpPage() {
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<main>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const target = e.target as typeof e.target & {
|
||||
email: { value: string };
|
||||
password: { value: string };
|
||||
};
|
||||
email: { value: string }
|
||||
password: { value: string }
|
||||
}
|
||||
|
||||
const email = target.email.value;
|
||||
const password = target.password.value;
|
||||
const email = target.email.value
|
||||
const password = target.password.value
|
||||
|
||||
await fetch("/api/sign-up", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
await fetch('/api/sign-up', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
}).then((res) => res.json());
|
||||
}).then((res) => res.json())
|
||||
|
||||
router.push("/");
|
||||
router.push('/')
|
||||
}}
|
||||
>
|
||||
<label htmlFor="email">Email</label>
|
||||
@@ -253,49 +242,49 @@ export default function SignUpPage() {
|
||||
<button type="submit">Sign up</button>
|
||||
</form>
|
||||
<div>
|
||||
{"Already have an account? "}
|
||||
{'Already have an account? '}
|
||||
<Link href="sign-in">
|
||||
<a>Sign in</a>
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Then add the following code to `/pages/sign-in.tsx`:
|
||||
|
||||
```
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
```ts
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function SignInPage() {
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<main>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const target = e.target as typeof e.target & {
|
||||
email: { value: string };
|
||||
password: { value: string };
|
||||
};
|
||||
email: { value: string }
|
||||
password: { value: string }
|
||||
}
|
||||
|
||||
const email = target.email.value;
|
||||
const password = target.password.value;
|
||||
const email = target.email.value
|
||||
const password = target.password.value
|
||||
|
||||
const { state, mfaUrl } = await fetch("/api/sign-in", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const { state, mfaUrl } = await fetch('/api/sign-in', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
}).then((res) => res.json());
|
||||
}).then((res) => res.json())
|
||||
|
||||
if (state === "CHALLENGE_REQUIRED") {
|
||||
window.location.href = mfaUrl;
|
||||
if (state === 'CHALLENGE_REQUIRED') {
|
||||
window.location.href = mfaUrl
|
||||
} else {
|
||||
router.push("/");
|
||||
router.push('/')
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -312,38 +301,38 @@ export default function SignInPage() {
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Now we will use Supabase's `withPageAuth` wrapper around `getServerSideProps` to make the home page require authentication via SSR. Replace the existing code in `/pages/index.tsx` with the following:
|
||||
|
||||
```
|
||||
import { getUser, User, withPageAuth } from "@supabase/auth-helpers-nextjs";
|
||||
import { GetServerSideProps } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { authsignal } from "../lib/authsignal";
|
||||
```ts
|
||||
import { getUser, User, withPageAuth } from '@supabase/auth-helpers-nextjs'
|
||||
import { GetServerSideProps } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { authsignal } from '../lib/authsignal'
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
isEnrolled: boolean;
|
||||
user: User
|
||||
isEnrolled: boolean
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = withPageAuth({
|
||||
redirectTo: "/sign-in",
|
||||
redirectTo: '/sign-in',
|
||||
async getServerSideProps(ctx) {
|
||||
const { user } = await getUser(ctx);
|
||||
const { user } = await getUser(ctx)
|
||||
|
||||
const { isEnrolled } = await authsignal.getUser({userId: user.id});
|
||||
const { isEnrolled } = await authsignal.getUser({ userId: user.id })
|
||||
|
||||
return {
|
||||
props: { user, isEnrolled },
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export default function HomePage({ user, isEnrolled }: Props) {
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<main>
|
||||
@@ -351,29 +340,29 @@ export default function HomePage({ user, isEnrolled }: Props) {
|
||||
<div> Signed in as: {user?.email}</div>
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
const { mfaUrl } = await fetch("/api/mfa", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const { mfaUrl } = await fetch('/api/mfa', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ isEnrolled }),
|
||||
}).then((res) => res.json());
|
||||
}).then((res) => res.json())
|
||||
|
||||
window.location.href = mfaUrl;
|
||||
window.location.href = mfaUrl
|
||||
}}
|
||||
>
|
||||
{isEnrolled ? "Manage MFA settings" : "Set up MFA"}
|
||||
{isEnrolled ? 'Manage MFA settings' : 'Set up MFA'}
|
||||
</button>
|
||||
<button onClick={() => router.push("/api/sign-out")}>Sign out</button>
|
||||
<button onClick={() => router.push('/api/sign-out')}>Sign out</button>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Optional: To make things look a bit nicer, you can add the following to `/styles/globals.css`:
|
||||
|
||||
```
|
||||
```css
|
||||
main {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
@@ -435,113 +424,95 @@ Now we'll replace the existing api routes in `/pages/api/` with 5 new routes:
|
||||
|
||||
Add the following code to `/pages/api/sign-in.ts`:
|
||||
|
||||
```
|
||||
import { supabaseClient } from "@supabase/auth-helpers-nextjs";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authsignal } from "../../lib/authsignal";
|
||||
import { setAuthCookie, setTempCookie } from "../../lib/cookies";
|
||||
```ts
|
||||
import { supabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { authsignal } from '../../lib/authsignal'
|
||||
import { setAuthCookie, setTempCookie } from '../../lib/cookies'
|
||||
|
||||
export default async function signIn(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { email, password } = req.body;
|
||||
export default async function signIn(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { email, password } = req.body
|
||||
|
||||
const { data, error } = await supabaseClient.auth.api.signInWithEmail(
|
||||
email,
|
||||
password
|
||||
);
|
||||
const { data, error } = await supabaseClient.auth.api.signInWithEmail(email, password)
|
||||
|
||||
if (error || !data?.user) {
|
||||
return res.send({ error });
|
||||
return res.send({ error })
|
||||
}
|
||||
|
||||
const { state, url: mfaUrl } = await authsignal.track({
|
||||
action: "signIn",
|
||||
action: 'signIn',
|
||||
userId: data.user.id,
|
||||
});
|
||||
})
|
||||
|
||||
if (state === "CHALLENGE_REQUIRED") {
|
||||
await setTempCookie(data, res);
|
||||
if (state === 'CHALLENGE_REQUIRED') {
|
||||
await setTempCookie(data, res)
|
||||
} else {
|
||||
setAuthCookie(data, res);
|
||||
setAuthCookie(data, res)
|
||||
}
|
||||
|
||||
res.send({ state, mfaUrl });
|
||||
res.send({ state, mfaUrl })
|
||||
}
|
||||
```
|
||||
|
||||
Then to handle new sign-ups add the following to `/pages/api/sign-up.ts`:
|
||||
|
||||
```
|
||||
import { supabaseClient } from "@supabase/auth-helpers-nextjs";
|
||||
import { Session } from "@supabase/supabase-js";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { setAuthCookie } from "../../lib/cookies";
|
||||
```ts
|
||||
import { supabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { setAuthCookie } from '../../lib/cookies'
|
||||
|
||||
export default async function signUp(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { email, password } = req.body;
|
||||
export default async function signUp(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { email, password } = req.body
|
||||
|
||||
const { data, error } = await supabaseClient.auth.api.signUpWithEmail(
|
||||
email,
|
||||
password
|
||||
);
|
||||
const { data, error } = await supabaseClient.auth.api.signUpWithEmail(email, password)
|
||||
|
||||
if (error || !isSession(data)) {
|
||||
res.send({ error });
|
||||
res.send({ error })
|
||||
} else {
|
||||
setAuthCookie(data, res);
|
||||
res.send({ data });
|
||||
setAuthCookie(data, res)
|
||||
res.send({ data })
|
||||
}
|
||||
}
|
||||
|
||||
const isSession = (data: any): data is Session => !!data?.access_token;
|
||||
const isSession = (data: any): data is Session => !!data?.access_token
|
||||
```
|
||||
|
||||
To clear the auth cookies on sign-out add the following to `/pages/api/sign-out.ts`:
|
||||
|
||||
```
|
||||
import { supabaseClient } from "@supabase/auth-helpers-nextjs";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
```ts
|
||||
import { supabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export default async function signOut(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
supabaseClient.auth.api.deleteAuthCookie(req, res, { redirectTo: "/sign-in" });
|
||||
export default async function signOut(req: NextApiRequest, res: NextApiResponse) {
|
||||
supabaseClient.auth.api.deleteAuthCookie(req, res, { redirectTo: '/sign-in' })
|
||||
}
|
||||
```
|
||||
|
||||
To handle the user's actions to set up MFA or manage their existing MFA settings, add the following to `/pages/api/mfa.ts`:
|
||||
|
||||
```
|
||||
import { getUser, withApiAuth } from "@supabase/auth-helpers-nextjs";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authsignal } from "../../lib/authsignal";
|
||||
```ts
|
||||
import { getUser, withApiAuth } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { authsignal } from '../../lib/authsignal'
|
||||
|
||||
export default withApiAuth(async function mfa(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).send({ message: "Only POST requests allowed" });
|
||||
export default withApiAuth(async function mfa(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).send({ message: 'Only POST requests allowed' })
|
||||
}
|
||||
|
||||
const { user } = await getUser({ req, res });
|
||||
const { user } = await getUser({ req, res })
|
||||
|
||||
const { isEnrolled } = req.body;
|
||||
const { isEnrolled } = req.body
|
||||
|
||||
const { url: mfaUrl } = await authsignal.track({
|
||||
action: isEnrolled ? "manageSettings" : "enroll",
|
||||
action: isEnrolled ? 'manageSettings' : 'enroll',
|
||||
userId: user.id,
|
||||
redirectToSettings: isEnrolled,
|
||||
});
|
||||
})
|
||||
|
||||
res.send({ mfaUrl });
|
||||
});
|
||||
res.send({ mfaUrl })
|
||||
})
|
||||
```
|
||||
|
||||
Because the user should be authenticated with Supabase to set up or manage MFA, we can use Supabase's `withApiAuth` wrapper to protect this route.
|
||||
@@ -550,28 +521,25 @@ The `redirectToSettings` param specifies whether the user should be redirected t
|
||||
|
||||
Finally we need a route to handle the redirect back from Authsignal after an MFA challenge. Add the following to `/pages/api/callback.ts`:
|
||||
|
||||
```
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authsignal } from "../../lib/authsignal";
|
||||
import { getSessionFromTempCookie, setAuthCookie } from "../../lib/cookies";
|
||||
```ts
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { authsignal } from '../../lib/authsignal'
|
||||
import { getSessionFromTempCookie, setAuthCookie } from '../../lib/cookies'
|
||||
|
||||
export default async function callback(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const token = req.query.token as string;
|
||||
export default async function callback(req: NextApiRequest, res: NextApiResponse) {
|
||||
const token = req.query.token as string
|
||||
|
||||
const { success } = await authsignal.validateChallenge({ token });
|
||||
const { success } = await authsignal.validateChallenge({ token })
|
||||
|
||||
if (success) {
|
||||
const session = await getSessionFromTempCookie(req);
|
||||
const session = await getSessionFromTempCookie(req)
|
||||
|
||||
if (session) {
|
||||
setAuthCookie(session, res);
|
||||
setAuthCookie(session, res)
|
||||
}
|
||||
}
|
||||
|
||||
res.redirect("/");
|
||||
res.redirect('/')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'onesignal',
|
||||
title: 'OneSignal',
|
||||
description:
|
||||
'OneSignal allows you to send cloud messages to your users. Combine OneSignal with your Supabase apps and you can reach out to your users whenever there is a change in your database.',
|
||||
}
|
||||
|
||||
[OneSignal](https://onesignal.com/) is a tool that allows you to send messages across different channels such as the following to keep your users engaged.
|
||||
|
||||
- Push notifications
|
||||
- SMS
|
||||
- Emails
|
||||
- In-app notifications
|
||||
|
||||
This guide steps you through how you can integrate OneSignal with Supabase to create a seamless cloud messaging experience for your users using Database webhooks and edge functions through a simple Next.js application.
|
||||
|
||||

|
||||
|
||||
We will create a simple ordering app and use Supabase Database Webhooks in conjunction with Edge Function to provide a real-time push notification experience.
|
||||
|
||||
You can find the complete example app along with the edge functions code to send the notifications [here](https://github.com/supabase-community/onesignal).
|
||||
|
||||

|
||||
|
||||
|
||||
## Step 1: Getting started
|
||||
|
||||
Before we dive into the code, this guide assumes that you have the following ready
|
||||
|
||||
- [Supabase](https://supabase.com/) project created
|
||||
- [OneSignal](https://onesignal.com/) app created
|
||||
- [Supabase CLI](https://supabase.com/docs/guides/cli) installed on your machine
|
||||
|
||||
Let’s create a Next.js app with tailwind CSS pre-installed
|
||||
|
||||
```bash
|
||||
npx create-next-app -e with-tailwindcss --ts
|
||||
```
|
||||
|
||||
We will then install the Supabase and OneSignal SDK.
|
||||
|
||||
```bash
|
||||
npm i @supabase/supabase-js
|
||||
npm i react-onesignal
|
||||
```
|
||||
|
||||
After that, follow the instructions [here](https://documentation.onesignal.com/docs/web-push-custom-code-setup) to set up OneSignal for the web. You can set the URL of the app as a local host if you want to run the app locally, or add a remote URL if you want to deploy your app to a public hosting. You should add the file you obtain in step 4 of the instruction under the `public` directory of your Next.js app like [this](https://github.com/supabase-community/onesignal/tree/main/app/public).
|
||||
|
||||
## Step 2: Build Next.js app
|
||||
|
||||
The Next.js app will have a login form for the user to sign in, and a button that they can press to make an order once they are signed in. Update the `index.tsx` file to the following.
|
||||
|
||||
```tsx title="pages/index.tsx"
|
||||
import { createClient, User } from '@supabase/supabase-js'
|
||||
import type { NextPage } from 'next'
|
||||
import Head from 'next/head'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import OneSignal from 'react-onesignal'
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
const oneSignalAppId = process.env.NEXT_PUBLIC_ONESIGNAL_APP_ID!
|
||||
|
||||
const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
|
||||
const [oneSignalInitialized, setOneSignalInitialized] =
|
||||
useState<boolean>(false)
|
||||
|
||||
/**
|
||||
* Initializes OneSignal SDK for a given Supabase User ID
|
||||
* @param uid Supabase User ID
|
||||
*/
|
||||
const initializeOneSignal = async (uid: string) => {
|
||||
if (oneSignalInitialized) {
|
||||
return
|
||||
}
|
||||
setOneSignalInitialized(true)
|
||||
await OneSignal.init({
|
||||
appId: oneSignalAppId,
|
||||
notifyButton: {
|
||||
enable: true,
|
||||
},
|
||||
|
||||
allowLocalhostAsSecureOrigin: true,
|
||||
})
|
||||
|
||||
await OneSignal.setExternalUserId(uid)
|
||||
}
|
||||
|
||||
const sendMagicLink = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
const { email } = Object.fromEntries(new FormData(event.currentTarget))
|
||||
if (typeof email !== 'string') return
|
||||
|
||||
const { error } = await supabase.auth.signInWithOtp({ email })
|
||||
if (error) {
|
||||
alert(error.message)
|
||||
} else {
|
||||
alert('Check your email inbox')
|
||||
}
|
||||
}
|
||||
|
||||
// Place a order with the selected price
|
||||
const submitOrder = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
const { price } = Object.fromEntries(new FormData(event.currentTarget))
|
||||
if (typeof price !== 'string') return
|
||||
|
||||
const { error } = await supabase
|
||||
.from('orders')
|
||||
.insert({ price: Number(price) })
|
||||
if (error) {
|
||||
alert(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const initialize = async () => {
|
||||
const initialUser = (await supabase.auth.getUser())?.data.user
|
||||
setUser(initialUser ?? null)
|
||||
if (initialUser) {
|
||||
initializeOneSignal(initialUser.id)
|
||||
}
|
||||
}
|
||||
|
||||
initialize()
|
||||
|
||||
const authListener = supabase.auth.onAuthStateChange(
|
||||
async (event, session) => {
|
||||
const user = session?.user ?? null
|
||||
setUser(user)
|
||||
if (user) {
|
||||
initializeOneSignal(user.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return () => {
|
||||
authListener.data.subscription.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>OneSignal Order Notification App</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<main className="flex items-center justify-center min-h-screen bg-black">
|
||||
{user ? (
|
||||
<form className="flex flex-col space-y-2" onSubmit={submitOrder}>
|
||||
<select
|
||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded block p-2"
|
||||
name="price"
|
||||
>
|
||||
<option value="100">$100</option>
|
||||
<option value="200">$200</option>
|
||||
<option value="300">$300</option>
|
||||
</select>
|
||||
<button
|
||||
type="submit"
|
||||
className="py-1 px-4 text-lg bg-green-400 rounded"
|
||||
>
|
||||
Place an Order
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<form className="flex flex-col space-y-2" onSubmit={sendMagicLink}>
|
||||
<input
|
||||
className="border-green-300 border rounded p-2 bg-transparent text-white"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="py-1 px-4 text-lg bg-green-400 rounded"
|
||||
>
|
||||
Send Magic Link
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
```
|
||||
|
||||
There is quite a bit of stuff going on here, but basically, it’s creating a simple UI for the user to sign in using the [magic link](https://supabase.com/docs/guides/auth/auth-magic-link), and once the user is signed in, will initialize OneSignal to ask the user to receive notifications on the website.
|
||||
|
||||
Notice that inside the `initializeOneSignal()` function, we are setting the Supabase user ID as an [external user ID of OneSignal](https://documentation.onesignal.com/docs/external-user-ids). This allows us to later send push notifications to the user using their Supabase user ID from the backend, which is very handy.
|
||||
|
||||
```tsx
|
||||
await OneSignal.setExternalUserId(uid);
|
||||
```
|
||||
|
||||
The front-end side of things is done here. Let’s get into the backend.
|
||||
|
||||
We also need to set our environment variables. Create a `.env.local` file and use the following template to set the environment variables. You can find your Supabase configuration in your dashboard under `settings > API`, and you can find the OneSignal app ID from `Settings > Keys & IDs`
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
|
||||
NEXT_PUBLIC_ONESIGNAL_APP_ID=YOUR_ONESIGNAL_APP_ID
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Step 3: Create the Edge Function
|
||||
|
||||
Let’s create an edge function that will receive [database webhooks](https://supabase.com/docs/guides/database/webhooks) from the database and calls the OneSignal API to send the push notification.
|
||||
|
||||
```bash
|
||||
supabase functions new notify
|
||||
```
|
||||
|
||||
Replace the contents of `supabase/functions/notify/index.ts` with the following
|
||||
|
||||
```tsx
|
||||
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
|
||||
import * as OneSignal from 'https://esm.sh/@onesignal/node-onesignal@1.0.0-beta7'
|
||||
|
||||
const _OnesignalAppId_ = Deno.env.get('ONESIGNAL_APP_ID')!
|
||||
const _OnesignalUserAuthKey_ = Deno.env.get('USER_AUTH_KEY')!
|
||||
const _OnesignalRestApiKey_ = Deno.env.get('ONESIGNAL_REST_API_KEY')!
|
||||
const configuration = OneSignal.createConfiguration({
|
||||
userKey: _OnesignalUserAuthKey_,
|
||||
appKey: _OnesignalRestApiKey_,
|
||||
})
|
||||
|
||||
const onesignal = new OneSignal.DefaultApi(configuration)
|
||||
|
||||
serve(async (req) => {
|
||||
try {
|
||||
const { record } = await req.json()
|
||||
|
||||
// Build OneSignal notification object
|
||||
const notification = new OneSignal.Notification()
|
||||
notification.app_id = _OnesignalAppId_
|
||||
notification.include_external_user_ids = [record.user_id]
|
||||
notification.contents = {
|
||||
en: `You just spent $${record.price}!`,
|
||||
}
|
||||
const onesignalApiRes = await onesignal.createNotification(notification)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ onesignalResponse: onesignalApiRes }),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('Failed to create OneSignal notification', err)
|
||||
return new Response('Server error.', {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
If you see bunch of errors in your editor, it's because your editor is not configured to use Deno. Follow the official setup guide [here](https://deno.land/manual@v1.28.3/getting_started/setup_your_environment) to setup your IDE to use Deno.
|
||||
|
||||
The function receives a `record` object, which is the row inserted in your `orders` table, and constructs a notification object to then send to OneSignal to deliver the push notification.
|
||||
|
||||
We also need to set the environment variable for the function. Create a `.env` file under your `supabase` directory and paste the following.
|
||||
|
||||
```bash
|
||||
ONESIGNAL_APP_ID=YOUR_ONESIGNAL_APP_ID
|
||||
USER_AUTH_KEY=YOUR_USER_AUTH_KEY
|
||||
ONESIGNAL_REST_API_KEY=YOUR_ONESIGNAL_REST_API_KEY
|
||||
```
|
||||
|
||||
`ONESIGNAL_APP_ID` and `ONESIGNAL_REST_API_KEY` can be found under `Settings > Keys & IDs` of your OneSignal app, and `USER_AUTH_KEY` can be found by going to `Account & API Keys` page by clicking your icon in the top right corner and scrolling to the `User Auth Key` section.
|
||||
|
||||

|
||||
|
||||
|
||||
Once your environment variables are filled in, you can run the following command to set the environment variable.
|
||||
|
||||
```bash
|
||||
supabase secrets set --env-file ./supabase/.env
|
||||
```
|
||||
|
||||
At this point, the function should be ready to be deployed! Run the following command to deploy your functions to the edge! The `no-verify-jwt` flag is required if you plan to call the function from a webhook.
|
||||
|
||||
```bash
|
||||
supabase functions deploy notify --no-verify-jwt
|
||||
```
|
||||
|
||||
## Step 4: Setting up the Supabase database
|
||||
|
||||
Finally, we get to set up the database! Run the following SQL to set up the `orders` table.
|
||||
|
||||
```sql
|
||||
create table if not exists public.orders (
|
||||
id uuid not null primary key default uuid_generate_v4(),
|
||||
created_at timestamptz not null default now(),
|
||||
user_id uuid not null default auth.uid(),
|
||||
price int8 not null
|
||||
);
|
||||
```
|
||||
|
||||
As you can see, the `orders` table has 4 columns and 3 of them have default values. That means all we need to send from the front-end app is the price. That is why our insert statement looked very simple.
|
||||
|
||||
```tsx
|
||||
const { error } = await supabase.from('orders').insert({
|
||||
price: 100,
|
||||
})
|
||||
```
|
||||
|
||||
Let’s also set up the webhook so that whenever a new row is inserted in the `orders` table, it calls the edge function. Go to `Database > Webhooks` and create a new Database Webhook. The table should be set to `orders` and Events should be inserted. The type should be HTTP Request, the HTTP method should be POST, and the URL should be the URL of your edge function. Hit confirm to save the webhook configuration.
|
||||
|
||||

|
||||
|
||||
At this point, the app should be complete! Run your app locally with `npm run dev`, or deploy your app to a hosting service and see how you receive a push notification when you place an order!
|
||||
Remember that if you decide to deploy your app to a hosting service, you would need to create another OneSignal app configured for your local address.
|
||||
|
||||

|
||||
|
||||
|
||||
## Resources
|
||||
|
||||
This particular example was using Next.js, but you can apply the same principles to implement send push notification, SMS, Emails, and in-app-notifications on other platforms as well.
|
||||
|
||||
- [OneSignal + Flutter + Supabase example](https://github.com/OneSignalDevelopers/onesignal-supabase-sample-integration-supabase)
|
||||
- [OneSignal Mobile Quickstart](https://documentation.onesignal.com/docs/mobile-sdk-setup)
|
||||
- [OneSignal Documentation](https://documentation.onesignal.com/docs/onesignal-platform)
|
||||
- [OneSignal Onboarding guide](https://documentation.onesignal.com/docs/onboarding-with-onesignal)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -39,12 +39,7 @@ Once you have added both to WeWeb, you will have the option to enable realtime t
|
||||
|
||||
**🚨 Warning 🚨**
|
||||
|
||||
> Realtime is disabled by default in Supabase for better database performance and security. Learn more about [realtime functionalities](https://supabase.com/docs/guides/database#realtime) in our documentation.
|
||||
> Member
|
||||
> @awalias awalias 16 days ago
|
||||
> might just wanna change these ones to be relative but otherwise looks great 👍
|
||||
|
||||
@EWMAs Reply...
|
||||
> Realtime is disabled by default in Supabase for better database performance and security. Learn more about [realtime functionalities](/docs/guides/realtime).
|
||||
|
||||
## Step 2: GET Data from Supabase
|
||||
|
||||
|
||||
@@ -1,84 +1,81 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { IconCheck } from 'ui'
|
||||
|
||||
export const meta = {
|
||||
title: 'Access Control',
|
||||
description: 'Roles and permissions at the organization level',
|
||||
}
|
||||
|
||||
Supabase provides granular access control features that let you manage permissions across your organizations.
|
||||
|
||||
Within a Supabase organization, a member can have one of the following roles:
|
||||
Supabase provides granular access controls to manage permissions across your organizations.
|
||||
For each organization, a member can have one of the following roles:
|
||||
|
||||
- Owner
|
||||
- Administrator
|
||||
- Developer
|
||||
|
||||
A default organization is created for a user when they first sign-in and
|
||||
assigned the **Owner** role. If the user wants to invite others
|
||||
to collaborate within the organization, they can visit the organization team
|
||||
settings (`https://app.supabase.com/org/<org-slug>/settings#team`) to send an
|
||||
invite link to another user's email. The invite expires after 24 hours.
|
||||
|
||||
Invites sent from a SSO account can only be accepted by another SSO account
|
||||
coming from the same identity provider. This is a security measure that
|
||||
prevents accidental invites to accounts not managed by your company's
|
||||
enterprise systems.
|
||||
|
||||
Project level invites are not available at this time. A member of the
|
||||
organization will be able to access all projects under the organization. If you
|
||||
wish to restrict access to certain projects, please create another organization
|
||||
to manage this.
|
||||
A default organization is created when you first sign in and
|
||||
you'll be assigned the **Owner** role.
|
||||
Each member can access all projects under the organization.
|
||||
Project level invites are not available at this time.
|
||||
Create a separate organization if you need to restrict access to certain projects.
|
||||
|
||||
## Manage team members
|
||||
|
||||
You can invite your team members into your organizations to collaborate on projects.
|
||||
To invite others to collaborate, visit your organization's team settings in the
|
||||
[Dashboard](https://app.supabase.com/projects) to send an invite link to
|
||||
another user's email. The invite expires after 24 hours.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/docs/videos/invite-team.mp4" type="video/mp4" muted playsInline />
|
||||
</video>
|
||||
### Transferring ownership of an organization
|
||||
|
||||
Each Supabase organization can have one or more owners. If you no longer want be an owner of an organization, click **Leave team** in the members view (`https://app.supabase.com/org/<org-slug>/settings#team`) of your organization.
|
||||
However, you can only leave an organization when there is _at least one other owner_.
|
||||
|
||||
If you are transferring ownership of your organization to someone else, you will need to invite the new member with the **Owner** role. You can leave the organization after they've accepted the invitation.
|
||||
|
||||
### Permissions across roles [#permission-across-roles]
|
||||
|
||||
The table below shows the corresponding permissions for each available role you can assign a team member in the Dashboard.
|
||||
|
||||
| Permissions | Owner | Administrator | Developer |
|
||||
| ------------------------ | ----- | ------------- | --------- |
|
||||
| Permissions | Owner | Administrator | Developer |
|
||||
| ------------------------ | ----------------------- | ----------------------- | ----------------------- |
|
||||
| **Organization** |
|
||||
| Change organization name | ✅ | | |
|
||||
| Delete organization | ✅ | | |
|
||||
| Change organization name | <IconCheck size={14} /> | | |
|
||||
| Delete organization | <IconCheck size={14} /> | | |
|
||||
| **Members** |
|
||||
| Add an Owner | ✅ | | |
|
||||
| Remove an Owner | ✅ | | |
|
||||
| Add an Administrator | ✅ | ✅ | |
|
||||
| Remove an Administrator | ✅ | ✅ | |
|
||||
| Add a Developer | ✅ | ✅ | |
|
||||
| Remove a Developer | ✅ | ✅ | |
|
||||
| Revoke an invite | ✅ | ✅ | |
|
||||
| Resend an invite | ✅ | ✅ | |
|
||||
| Accept an invite[^1] | ✅ | ✅ | ✅ |
|
||||
| Add an Owner | <IconCheck size={14} /> | | |
|
||||
| Remove an Owner | <IconCheck size={14} /> | | |
|
||||
| Add an Administrator | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Remove an Administrator | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Add a Developer | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Remove a Developer | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Revoke an invite | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Resend an invite | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Accept an invite[^1] | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
|
||||
| **Billing** |
|
||||
| Read invoices | ✅ | ✅ | ✅ |
|
||||
| Read billing email | ✅ | ✅ | ✅ |
|
||||
| Change billing email | ✅ | | |
|
||||
| View subscription | ✅ | ✅ | ✅ |
|
||||
| Update subscription | ✅ | ✅ | |
|
||||
| Read billing address | ✅ | ✅ | ✅ |
|
||||
| Update billing address | ✅ | ✅ | |
|
||||
| Read tax codes | ✅ | ✅ | ✅ |
|
||||
| Update tax codes | ✅ | ✅ | |
|
||||
| Read payment methods | ✅ | ✅ | ✅ |
|
||||
| Update payment methods | ✅ | ✅ | |
|
||||
| Read invoices | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
|
||||
| Read billing email | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
|
||||
| Change billing email | <IconCheck size={14} /> | | |
|
||||
| View subscription | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
|
||||
| Update subscription | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Read billing address | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
|
||||
| Update billing address | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Read tax codes | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
|
||||
| Update tax codes | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Read payment methods | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
|
||||
| Update payment methods | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| **Projects** |
|
||||
| Create a project | ✅ | ✅ | |
|
||||
| Delete a project | ✅ | ✅ | |
|
||||
| Update a project | ✅ | ✅ | |
|
||||
| Pause a project | ✅ | ✅ | |
|
||||
| Resume a project | ✅ | ✅ | |
|
||||
| Restart a project | ✅ | ✅ | ✅ |
|
||||
| Create a project | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Delete a project | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Update a project | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Pause a project | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Resume a project | <IconCheck size={14} /> | <IconCheck size={14} /> | |
|
||||
| Restart a project | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
|
||||
|
||||
[^1]:
|
||||
If the invite was sent from a SSO account, it can only be accepted from
|
||||
a user signed in via the same identity provider.
|
||||
Invites sent from a SSO account can only be accepted by another SSO account
|
||||
coming from the same identity provider. This is a security measure that
|
||||
prevents accidental invites to accounts not managed by your company's
|
||||
enterprise systems.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Database Backups',
|
||||
description: 'Learn about the available backup methods for your Supabase project.',
|
||||
}
|
||||
|
||||
Database backups are an integral part of any disaster recovery plan. Disasters come in many shapes and sizes. It could be as simple as accidentally deleting a table column, the database crashing, or even a natural calamity wiping out the underlying hardware a database is running on. The risks and impact brought by these scenarios can never be fully eliminated, but only minimized or even mitigated. Having database backups is a form of insurance policy. They are essentially snapshots of the database at various points in time. When disaster strikes, database backups allow the project to be brought back to any of these points in time, therefore averting the crisis.
|
||||
|
||||
## Frequency of Backups
|
||||
|
||||
When deciding how often a database should be backed up, the key business metric Recovery Point Objective (RPO) should be considered. RPO is the threshold for how much data, measured in time, a business could lose when disaster strikes. This amount is fully dependent on a business and its underlying requirements. A low RPO would mean that database backups would have to be taken at an increased cadence throughout the day. Each Supabase project has access to two forms of backups, Daily Backups and Point-in-Time Recovery. The agreed upon RPO would be a deciding factor in choosing which solution best fits a project.
|
||||
|
||||
<Admonition type="note">
|
||||
Database backups do not include objects stored via the Storage API, as the database only includes
|
||||
metadata about these objects. Restoring an old backup does not restore objects that have been
|
||||
deleted since then.
|
||||
</Admonition>
|
||||
|
||||
## Daily Backups
|
||||
|
||||
All Pro and Enterprise tier Supabase projects are backed up automatically on a daily basis. In terms of Recovery Point Objective (RPO), Daily Backups would be suitable for projects willing to lose up to 24 hours worth of data if disaster hits at the most inopportune time. If a lower RPO is required, enabling Point-in-Time Recovery should be considered.
|
||||
|
||||
### Backup Process [#daily-backups-process]
|
||||
|
||||
The PostgreSQL utility [pg_dumpall](https://www.postgresql.org/docs/current/app-pg-dumpall.html) is used to perform daily backups. An SQL file is generated, zipped up, and sent to our storage servers for safe keeping.
|
||||
|
||||

|
||||
|
||||
You can access daily backups in the [Scheduled backups](https://app.supabase.com/project/_/database/backups/scheduled) settings in the Dashboard. Pro tier projects can access the last 7 days’ worth of daily backups while Enterprise tier projects can access up to 30 days’ worth of daily backups. Users can restore their project to any one of the backups or download them as a zipped SQL file.
|
||||
|
||||
### Restoration Process [#daily-backups-restoration-process]
|
||||
|
||||
When selecting a backup to restore to, select the closest available one made before the desired point in time to restore to. Earlier backups can always be chosen too but do consider the number of days’ worth of data that could be lost.
|
||||
|
||||

|
||||
|
||||
The Dashboard will then prompt for a confirmation before proceeding with the restoration. The project will be inaccessible following this. As such, do ensure to allot downtime beforehand. This is dependent on the size of the database. The larger it is, the longer the downtime will be. Once the confirmation has been given, the underlying SQL of the chosen backup is then run against the project. The PostgreSQL utility [psql](https://www.postgresql.org/docs/current/app-psql.html) is used to facilitate the restoration. The Dashboard will display a notification once the restoration completes.
|
||||
|
||||
{/* screenshot of the Dashboard of the project completing restoration */}
|
||||
|
||||
## Point-in-Time Recovery
|
||||
|
||||
Point-in-Time Recovery (PITR) allows a project to be backed up at much shorter intervals. This provides users an option to restore to any chosen point of up to seconds in granularity. Even with daily backups, a day’s worth of data could still be lost. With PITR, backups could be performed up to the point of disaster.
|
||||
|
||||
<Admonition type="note">
|
||||
This feature is available to all Enterprise tier projects. Pro tier projects can enable PITR as an
|
||||
add-on.
|
||||
</Admonition>
|
||||
|
||||
### Backup Process [#pitr-backup-process]
|
||||
|
||||
As discussed [here](https://supabase.com/blog/postgresql-physical-logical-backups), PITR is made possible by a combination of taking physical backups of a project, as well as archiving [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) files. Physical backups provide a snapshot of the underlying directory of the database, while WAL files contain records of every change made in the database.
|
||||
|
||||
Supabase uses [WAL-G](https://github.com/wal-g/wal-g), an open source archival and restoration tool, to handle both aspects of PITR. On a daily basis, a snapshot of the database is taken and sent to our storage servers. Throughout the day, as database transactions occur, WAL files are generated and uploaded.
|
||||
|
||||
By default, WAL files are backed up at two minute intervals. If these files cross a certain file size threshold, they are backed up immediately. As such, during periods of high amount of transactions, WAL file backups become more frequent. Conversely, when there is no activity in the database, WAL file backups are not made. Overall, this would mean that at the worst case scenario or disaster, the PITR achieves a Recovery Point Objective (RPO) of two minutes.
|
||||
|
||||

|
||||
|
||||
You can access PITR in the [Point in Time](https://app.supabase.com/project/_/database/backups/pitr) settings in the Dashboard. The recovery period of a project is indicated by the earliest and latest points of recoveries displayed in one’s preferred timezone. If need be, the maximum amount of this recovery period can be modified accordingly.
|
||||
|
||||
Note that the latest restore point of the project could be significantly far from the current time. This occurs when there has not been any recent activity in the database, and therefore no WAL file backups have been made recently. This is perfectly fine as the state of the database at the latest point of recovery would still be indicative of the state of the database at the current time given that no transactions have been made in between.
|
||||
|
||||
### Restoration Process [#pitr-restoration-process]
|
||||
|
||||

|
||||
|
||||
A date and time picker will be provided upon pressing the `Start a restore` button. The process will only proceed if the selected date and time fall within the earliest and latest points of recoveries.
|
||||
|
||||

|
||||
|
||||
After locking in the desired point in time to recover to, The Dashboard will then prompt for a review and confirmation before proceeding with the restoration. The project will be inaccessible following this. As such, do ensure to allot for downtime beforehand. This is dependent on the size of the database. The larger it is, the longer the downtime will be. Once the confirmation has been given, the latest physical backup available is downloaded to the project and the database is partially restored. WAL files generated after this physical backup up to the specified point-in-time are then downloaded. The underlying records of transactions in these files are replayed against the database to complete the restoration. The Dashboard will display a notification once the restoration completes.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -12,18 +12,20 @@ Custom Domains are currently in beta, and are being slowly made available to pro
|
||||
|
||||
</Admonition>
|
||||
|
||||
Custom domains allow you to present a branded experience to your users. Currently, you must use a subdomain (e.g., `api.example.com`, rather than `example.com`) for the purposes of this guide.
|
||||
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.
|
||||
|
||||
## Configure a Custom Domain using the Supabase Dashboard
|
||||
|
||||
Follow the **Custom Domains** steps in the [General Settings](https://app.supabase.com/project/_/settings/general) page in the Dashboard to set up a custom domain for your project.
|
||||
|
||||
## Configure a Custom Domain using the Supabase CLI
|
||||
|
||||
This example assumes your Supabase project is `foobarbaz` with a corresponding API URL `foobarbaz.supabase.co` and configures a custom domain at `api.example.com`.
|
||||
|
||||
To get started:
|
||||
|
||||
1. [Install](/docs/guides/cli) the Supabase CLI 1.11.3+.
|
||||
1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform#manage-team-members) for the project you'd like to set up a custom domain for.
|
||||
1. Enable the Custom Domains add-on in the Billing page of the Dashboard.
|
||||
|
||||
## Configure a Custom Domain
|
||||
|
||||
This example assumes your Supabase project is `foobarbaz` with a corresponding API URL `foobarbaz.supabase.co` and configures a custom domain at `api.example.com`.
|
||||
1. [Install](/docs/guides/resources/supabase-cli) the Supabase CLI 1.11.3+.
|
||||
1. [Log in](/docs/guides/resources/supabase-cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
|
||||
### Configure a CNAME
|
||||
|
||||
@@ -47,7 +49,7 @@ Required outstanding validation records:
|
||||
api.example.com TXT -> ca3-F1HvR9i938OgVwpCFwi1jTsbhe1hvT0Ic3efPY3Q
|
||||
```
|
||||
|
||||
[^1] One of the records requires you to _replace_ the CNAME record set up in [the first step](#cname-configure) with a TXT record. You'll be able to restore it back to the CNAME after the verification process has been completed.
|
||||
[^1] One of the records requires you to _replace_ the CNAME record set up in [the first step](#configure-a-cname) with a TXT record. You'll be able to restore it back to the CNAME after the verification process has been completed.
|
||||
|
||||
### Verify your domain
|
||||
|
||||
@@ -97,9 +99,9 @@ Vanity Subdomains allow you to present a basic branded experience, compared to c
|
||||
|
||||
To get started:
|
||||
|
||||
1. [Install](/docs/guides/cli) the Supabase CLI 1.22.0+.
|
||||
1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/hosting/platform#manage-team-members) for the project you'd like to set up a vanity subdomain for.
|
||||
1. [Install](/docs/guides/resources/supabase-cli) the Supabase CLI 1.22.0+.
|
||||
1. [Log in](/docs/guides/resources/supabase-cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project you'd like to set up a vanity subdomain for.
|
||||
1. Ensure that your project has a paid subscription (Pro/Pay as you go/Enterprise tier) in the [Billing page of the Dashboard](https://app.supabase.com/project/_/settings/billing/subscription).
|
||||
|
||||
## Configure a Vanity Subdomain
|
||||
@@ -141,7 +143,8 @@ As with the final activation stage of the process for setting up a vanity subdom
|
||||
|
||||
## Limitations
|
||||
|
||||
Edge functions do not honor the custom domain or the vanity subdomain setting and they still have to be invoked via the `foobarbaz.supabase.co` domain.
|
||||
- Edge functions do not honor the custom domain or the vanity subdomain setting and they still have to be invoked via the `foobarbaz.supabase.co` domain.
|
||||
- A Supabase project can—at this time—use either a Custom Domain or a Vanity Subdomain, but not both.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -54,10 +54,31 @@ After developing your project and deciding it's Production Ready, you should run
|
||||
- Supabase employs a number of safeguards against bursts of incoming traffic to prevent abuse and help maximize stability across the platform
|
||||
- If you're expecting high load events including production launches or heavy load testing, or prolonged high resource usage please give us at least 2 weeks notice. You can do this by opening a ticket via the [support form](https://app.supabase.com/support/new).
|
||||
|
||||
### Rate Limits
|
||||
|
||||
- The table below shows the rate limit quotas on the following authentication endpoints:
|
||||
|
||||
| Endpoint | Path | Limited By | Rate Limit |
|
||||
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------- |
|
||||
| All endpoints that send emails | `/auth/v1/signup` `/auth/v1/recover` `/auth/v1/user`[^1] | Sum of combined requests | Defaults to 30 emails per hour. Is customizable with custom SMTP set up. |
|
||||
| All endpoints that send One-Time-Passwords (OTP) | `/auth/v1/otp` | Sum of combined requests | Defaults to 30 OTPs per hour. Is customizable. |
|
||||
| Send OTPs or magiclinks | `/auth/v1/otp` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Signup confirmation request | `/auth/v1/signup` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Password Reset Request | `/auth/v1/recover` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Verification requests | `/auth/v1/verify` | IP Address | 360 requests per hour (with bursts up to 30 requests) |
|
||||
| Token refresh requests | `/auth/v1/token` | IP Address | 360 requests per hour (with bursts up to 30 requests) |
|
||||
| Create or Verify an MFA challenge | `/auth/v1/factors/:id/challenge` `/auth/v1/factors/:id/verify` | IP Address | 15 requests per minute (with bursts up to 30 requests) |
|
||||
|
||||
### Abuse Prevention
|
||||
|
||||
- Supabase provides CAPTCHA protection on the signup, sign-in and password reset endpoints. Please refer to [our guide](/docs/guides/auth/auth-captcha) on how to protect against abuse using this method.
|
||||
|
||||
## Next steps
|
||||
|
||||
This checklist is always growing so be sure to check back frequently, and also feel free to suggest additions and amendments by making a PR on [GitHub](https://github.com/supabase/supabase).
|
||||
|
||||
[^1]: The rate limit is only applied on `/auth/v1/user` if this endpoint is called to update the user's email address.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -20,7 +20,7 @@ To get started:
|
||||
|
||||
1. [Install](/docs/guides/cli) the Supabase CLI 1.22.0+.
|
||||
1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/hosting/platform#manage-team-members) for the project that you are enabling network restrictions.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/hosting/platform/access-control#manage-team-members) for the project that you are enabling network restrictions.
|
||||
|
||||
## Check restrictions
|
||||
|
||||
|
||||
@@ -63,7 +63,14 @@ const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY
|
||||
*/
|
||||
const channel = supabase
|
||||
.channel('schema-db-changes')
|
||||
.on('postgres_changes', { event: '*', schema: 'public' }, (payload) => console.log(payload))
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
},
|
||||
(payload) => console.log(payload)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
@@ -74,8 +81,14 @@ To listen to changes on a table in the `public` schema:
|
||||
|
||||
const channel = supabase
|
||||
.channel('table-db-changes')
|
||||
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, (payload) =>
|
||||
console.log(payload)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'messages',
|
||||
},
|
||||
(payload) => console.log(payload)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
@@ -109,11 +122,21 @@ const channel = supabase
|
||||
.channel('db-changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: '*', schema: 'public', table: 'messages', filter: 'body=eq.bye' },
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'messages',
|
||||
filter: 'body=eq.bye',
|
||||
},
|
||||
(payload) => console.log(payload)
|
||||
)
|
||||
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'users' }, (payload) =>
|
||||
console.log(payload)
|
||||
.on('postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'users',
|
||||
},
|
||||
(payload) => console.log(payload)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
@@ -76,16 +76,6 @@ export const meta = {
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
### Self-Hosting Supabase
|
||||
|
||||
We're happy to host your next big thing, but we also make it easy to host Supabase on your own. We use Docker to help make setup as painless as possible.
|
||||
|
||||
[Learn more](/docs/guides/resources/self-hosting)
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* end of container */}
|
||||
@@ -112,25 +102,25 @@ export const migrationGuides = [
|
||||
{
|
||||
title: 'Firebase Auth',
|
||||
icon: '/docs/img/icons/firebase-icon',
|
||||
href: '/guides/migrations/firebase-auth',
|
||||
href: '/guides/resources/migrating-to-supabase/firebase-auth',
|
||||
description: 'Move your auth users from a Firebase project to a Supabase project.',
|
||||
},
|
||||
{
|
||||
title: 'Firestore Data',
|
||||
icon: '/docs/img/icons/firebase-icon',
|
||||
href: '/guides/migrations/firestore-data',
|
||||
href: '/guides/resources/migrating-to-supabase/firestore-data',
|
||||
description: 'Migrate the contents of a Firestore collection to a single PostgreSQL table.',
|
||||
},
|
||||
{
|
||||
title: 'Firebase Storage',
|
||||
icon: '/docs/img/icons/firebase-icon',
|
||||
href: '/guides/migrations/firebase-storage',
|
||||
href: '/guides/resources/migrating-to-supabase/firebase-storage',
|
||||
description: 'Convert your Firebase Storage files to Supabase Storage.',
|
||||
},
|
||||
{
|
||||
title: 'Heroku',
|
||||
icon: '/docs/img/icons/heroku-icon',
|
||||
href: '/guides/migrations/heroku',
|
||||
href: '/guides/resources/migrating-to-supabase/heroku',
|
||||
description: 'Migrate your Heroku Postgres database to Supabase.',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -173,7 +173,7 @@ Use the seed script in `supabase/seed.sql` (created with [`supabase init`](/docs
|
||||
-- in supabase/seed.sql
|
||||
insert into public.employees (name)
|
||||
values
|
||||
('Erlich Backman'),
|
||||
('Erlich Bachman'),
|
||||
('Richard Hendricks'),
|
||||
('Monica Hall');
|
||||
```
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'overview',
|
||||
title: 'Self Hosting',
|
||||
sidebar_label: 'Overview',
|
||||
description: 'Getting started with Self Hosting.',
|
||||
title: 'Self-Hosting',
|
||||
description: 'Getting started with self-hosting Supabase.',
|
||||
}
|
||||
|
||||
There are several ways to use Supabase:
|
||||
|
||||
- [Supabase Cloud](https://app.supabase.com): you don't need to deploy anything. We will manage and scale your infrastructure.
|
||||
- [Docker](../../guides/hosting/docker): deploy to your own infrastructure.
|
||||
- [Docker](/docs/guides/self-hosting/docker): deploy to your own infrastructure.
|
||||
- Kubernetes: coming soon.
|
||||
|
||||
## Architecture
|
||||
@@ -151,7 +149,7 @@ We realize that database migrations are difficult, and this is one of the proble
|
||||
|
||||
While Supabase officially supports Docker, we have several other deployment strategies managed by the community:
|
||||
|
||||
- [supabase-docker](../../guides/hosting/docker) (Official)
|
||||
- [supabase-docker](/docs/guides/self-hosting/docker) (Official)
|
||||
- [supabase-kubernetes](https://github.com/supabase-community/supabase-kubernetes) (Unofficial)
|
||||
- [supabase-terraform](https://github.com/supabase-community/supabase-terraform) (Unofficial)
|
||||
- [supabase-traefik](https://github.com/supabase-community/supabase-traefik) (Unofficial)
|
||||
@@ -0,0 +1,38 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { getAuthConfigV1 } from '~/lib/mdx/getConfig'
|
||||
import Param from '~/components/Params'
|
||||
|
||||
export const spec = getAuthConfigV1()
|
||||
|
||||
export const meta = {
|
||||
title: 'Auth Self-hosting Config',
|
||||
description: 'How to configure and deploy Supabase.',
|
||||
}
|
||||
|
||||
<div>{spec.info.description}</div>
|
||||
|
||||
<div>
|
||||
{spec.info.tags.map(tag => {
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-scale-1200">{tag.title}</h2>
|
||||
<p className="text-scale-1000">{tag.description}</p>
|
||||
<div className="not-prose">
|
||||
<h5 className="text-base text-scale-1200 mb-3">Parameters</h5>
|
||||
<ul>
|
||||
{spec.parameters.filter(param => param.tags.includes(tag.id)).map((param) => {
|
||||
return <Param name={param.title} type={param.type} description={param.description} required={param.required} />
|
||||
})}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
})}
|
||||
|
||||
</div>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -1,10 +1,8 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'docker',
|
||||
title: 'Self-hosting with Docker',
|
||||
description: 'How to configure and deploy Supabase.',
|
||||
sidebar_label: 'Docker',
|
||||
title: 'Self-Hosting with Docker',
|
||||
description: 'Learn how to configure and deploy Supabase with Docker.',
|
||||
}
|
||||
|
||||
Docker is the easiest way to get started with self-hosted Supabase.
|
||||
@@ -13,7 +11,7 @@ Docker is the easiest way to get started with self-hosted Supabase.
|
||||
|
||||
You need the following installed in your system:
|
||||
|
||||
- [Docker](https://docs.docker.com/engine/install/) and [docker-compose](https://docs.docker.com/compose/install/)
|
||||
- [Docker](https://docs.docker.com/engine/install/) and [docker compose](https://docs.docker.com/compose/install/)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
## Quick Start
|
||||
@@ -33,7 +31,7 @@ cd supabase/docker
|
||||
cp .env.example .env
|
||||
|
||||
# Start
|
||||
docker-compose up
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Now visit [http://localhost:3000](http://localhost:3000) to start using Supabase Studio.
|
||||
@@ -42,11 +40,11 @@ Now visit [http://localhost:3000](http://localhost:3000) to start using Supabase
|
||||
|
||||
While we provided you with some example secrets for getting started, you should NEVER deploy your Supabase setup using the defaults we have provided.
|
||||
|
||||
Please follow these steps to secure your Docker setup. We [strongly recommend](../../guides/hosting/overview#managing-your-secrets) using a secrets manager when deploying to production.
|
||||
Follow these steps to secure your Docker setup. We strongly recommend using a [secrets manager](../self-hosting#managing-your-secrets) when deploying to production.
|
||||
|
||||
### Generate API Keys
|
||||
|
||||
Use your `JWT_SECRET` to generate a `anon` and `service` API keys using the [JWT generator](../../guides/hosting/overview#api-keys).
|
||||
Use your `JWT_SECRET` to generate a `anon` and `service` API keys using the [JWT generator](../self-hosting#api-keys).
|
||||
|
||||
Replace the values in these files:
|
||||
|
||||
@@ -72,7 +70,7 @@ The Docker setup doesn't include a management database for managing users and lo
|
||||
|
||||
## Configuration
|
||||
|
||||
Each system can be [configured](../../guides/hosting/overview#configuration) to suit your particular use-case.
|
||||
Each system can be [configured](../self-hosting#configuration) to suit your particular use-case.
|
||||
|
||||
To keep the setup simple, we made some choices that may not be optimal for production:
|
||||
|
||||
@@ -82,7 +80,7 @@ To keep the setup simple, we made some choices that may not be optimal for produ
|
||||
|
||||
### Using an external database
|
||||
|
||||
We strongly [recommend](../../guides/hosting/overview#managing-your-database) that you decouple your database from `docker-compose` before deploying.
|
||||
We strongly recommend [decoupling your database](../self-hosting#managing-your-database) from `docker-compose` before deploying.
|
||||
The middleware will run with any PostgreSQL database that has logical replication enabled. The following environment variables should be updated
|
||||
in the `.env` file to point to your external database:
|
||||
|
||||
@@ -99,9 +97,13 @@ Once you have done this, you can safely comment out the `db` section of the `doc
|
||||
|
||||
### Setting database's `log_min_messages`
|
||||
|
||||
By default, `docker-compose` sets the database's `log_min_messages` configuration to `fatal` to prevent redundant logs generated by Realtime.
|
||||
By default, `docker compose` sets the database's `log_min_messages` configuration to `fatal` to prevent redundant logs generated by Realtime.
|
||||
However, you might miss important log messages such as database errors. Configure `log_min_messages` based on your needs.
|
||||
|
||||
### File storage backend on macOS
|
||||
|
||||
By default, Storage backend is set to `file`, which is to use local files as the storage backend. To make it work on macOS, you need to choose `VirtioFS` as the Docker container file sharing implementation (in Docker Desktop -> Preferences -> General).
|
||||
|
||||
## Deploying
|
||||
|
||||
See the following guides to deploy Docker Compose setup using your preferred tool and platform:
|
||||
@@ -0,0 +1,38 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { getRealtimeConfigV0 } from '~/lib/mdx/getConfig'
|
||||
import Param from '~/components/Params'
|
||||
|
||||
export const spec = getRealtimeConfigV0()
|
||||
|
||||
export const meta = {
|
||||
title: 'Realtime Self-hosting Config',
|
||||
description: 'How to configure and deploy Supabase.',
|
||||
}
|
||||
|
||||
<div>{spec.info.description}</div>
|
||||
|
||||
<div>
|
||||
{spec.info.tags.map(tag => {
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-scale-1200">{tag.title}</h2>
|
||||
<p className="text-scale-1000">{tag.description}</p>
|
||||
<div className="not-prose">
|
||||
<h5 className="text-base text-scale-1200 mb-3">Parameters</h5>
|
||||
<ul>
|
||||
{spec.parameters.filter(param => param.tags.includes(tag.id)).map((param) => {
|
||||
return <Param name={param.title} type={param.type} description={param.description} required={param.required} />
|
||||
})}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
})}
|
||||
|
||||
</div>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,38 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { getStorageConfigV0 } from '~/lib/mdx/getConfig'
|
||||
import Param from '~/components/Params'
|
||||
|
||||
export const spec = getStorageConfigV0()
|
||||
|
||||
export const meta = {
|
||||
title: 'Storage Self-hosting Config',
|
||||
description: 'How to configure and deploy Supabase.',
|
||||
}
|
||||
|
||||
<div>{spec.info.description}</div>
|
||||
|
||||
<div>
|
||||
{spec.info.tags.map(tag => {
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-scale-1200">{tag.title}</h2>
|
||||
<p className="text-scale-1000">{tag.description}</p>
|
||||
<div className="not-prose">
|
||||
<h5 className="text-base text-scale-1200 mb-3">Parameters</h5>
|
||||
<ul>
|
||||
{spec.parameters.filter(param => param.tags.includes(tag.id)).map((param) => {
|
||||
return <Param name={param.title} type={param.type} description={param.description} required={param.required} />
|
||||
})}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
})}
|
||||
|
||||
</div>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -144,7 +144,7 @@ export const meta = {
|
||||
key={product.title}
|
||||
passHref
|
||||
>
|
||||
<a className={'col-span-12 md:col-span-4'}>
|
||||
<a className={'col-span-12 md:col-span-6'}>
|
||||
<GlassPanel {...product} background={false} showIconBg={true}>
|
||||
{product.description}
|
||||
</GlassPanel>
|
||||
@@ -243,6 +243,7 @@ export const apis = [
|
||||
hasLightIcon: true,
|
||||
href: 'reference/javascript/introduction',
|
||||
description: 'Official client libraries for JavaScript and TypeScript.',
|
||||
span: 'col-span-12 md:col-span-6',
|
||||
},
|
||||
{
|
||||
title: 'Flutter Client Library',
|
||||
@@ -250,6 +251,7 @@ export const apis = [
|
||||
hasLightIcon: true,
|
||||
href: 'reference/dart/introduction',
|
||||
description: 'Official client libraries for Flutter and Dart.',
|
||||
span: 'col-span-12 md:col-span-6',
|
||||
},
|
||||
{
|
||||
title: 'Management API',
|
||||
@@ -257,6 +259,7 @@ export const apis = [
|
||||
hasLightIcon: true,
|
||||
href: 'reference/api/introduction',
|
||||
description: 'Manage your Supabase projects and organizations programmatically.',
|
||||
span: 'col-span-12 md:col-span-6',
|
||||
},
|
||||
{
|
||||
title: 'Supabase CLI',
|
||||
@@ -265,6 +268,7 @@ export const apis = [
|
||||
href: 'reference/cli/introduction',
|
||||
description:
|
||||
'Use the CLI to develop your project locally, manage database migrations, and deploy to the Supabase Platform or self-host.',
|
||||
span: 'col-span-12 md:col-span-6',
|
||||
},
|
||||
{
|
||||
title: 'Self-Hosting Auth',
|
||||
@@ -307,6 +311,13 @@ export const platform = [
|
||||
href: 'guides/resources',
|
||||
description: 'View examples, curated content from the community, migration guides, and more.',
|
||||
},
|
||||
{
|
||||
title: 'Self-Hosting Supabase',
|
||||
icon: '/docs/img/icons/menu/platform',
|
||||
hasLightIcon: true,
|
||||
href: 'guides/self-hosting',
|
||||
description: 'Learn how to self-host Supabase and deploy to your own infrastructure.',
|
||||
},
|
||||
{
|
||||
// header: '/docs/img/cards/sample-card-header-2.svg',
|
||||
title: 'Integrations',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { flattenSections } from '~/lib/helpers'
|
||||
import handleRefGetStaticPaths from '~/lib/mdx/handleRefStaticPaths'
|
||||
import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps'
|
||||
import { useRouter } from 'next/router'
|
||||
import RefSEO from '~/components/reference/RefSEO'
|
||||
|
||||
const sections = flattenSections(clientLibsCommonSections)
|
||||
|
||||
@@ -15,13 +16,21 @@ export default function JSReference(props) {
|
||||
const slug = router.query.slug[0]
|
||||
const filteredSection = sections.filter((section) => section.id === slug)
|
||||
|
||||
const pageTitle = filteredSection[0]?.title
|
||||
? `${filteredSection[0]?.title} | Supabase`
|
||||
: 'Supabase'
|
||||
|
||||
return (
|
||||
<RefSectionHandler
|
||||
sections={filteredSection}
|
||||
spec={spec}
|
||||
typeSpec={typeSpec}
|
||||
pageProps={props}
|
||||
/>
|
||||
<>
|
||||
<RefSEO title={pageTitle} />
|
||||
|
||||
<RefSectionHandler
|
||||
sections={filteredSection}
|
||||
spec={spec}
|
||||
typeSpec={typeSpec}
|
||||
pageProps={props}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { flattenSections } from '~/lib/helpers'
|
||||
import handleRefGetStaticPaths from '~/lib/mdx/handleRefStaticPaths'
|
||||
import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps'
|
||||
import { useRouter } from 'next/router'
|
||||
import RefSEO from '~/components/reference/RefSEO'
|
||||
|
||||
const sections = flattenSections(clientLibsCommonSections)
|
||||
|
||||
@@ -15,13 +16,20 @@ export default function JSReference(props) {
|
||||
const slug = router.query.slug[0]
|
||||
const filteredSection = sections.filter((section) => section.id === slug)
|
||||
|
||||
const pageTitle = filteredSection[0]?.title
|
||||
? `${filteredSection[0]?.title} | Supabase`
|
||||
: 'Supabase'
|
||||
return (
|
||||
<RefSectionHandler
|
||||
sections={filteredSection}
|
||||
spec={spec}
|
||||
typeSpec={typeSpec}
|
||||
pageProps={props}
|
||||
/>
|
||||
<>
|
||||
<RefSEO title={pageTitle} />
|
||||
|
||||
<RefSectionHandler
|
||||
sections={filteredSection}
|
||||
spec={spec}
|
||||
typeSpec={typeSpec}
|
||||
pageProps={props}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { flattenSections } from '~/lib/helpers'
|
||||
import handleRefGetStaticPaths from '~/lib/mdx/handleRefStaticPaths'
|
||||
import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps'
|
||||
import { useRouter } from 'next/router'
|
||||
import RefSEO from '~/components/reference/RefSEO'
|
||||
|
||||
const sections = flattenSections(clientLibsCommonSections)
|
||||
|
||||
@@ -15,13 +16,21 @@ export default function JSReference(props) {
|
||||
const slug = router.query.slug[0]
|
||||
const filteredSection = sections.filter((section) => section.id === slug)
|
||||
|
||||
const pageTitle = filteredSection[0]?.title
|
||||
? `${filteredSection[0]?.title} | Supabase`
|
||||
: 'Supabase'
|
||||
|
||||
return (
|
||||
<RefSectionHandler
|
||||
sections={filteredSection}
|
||||
spec={spec}
|
||||
typeSpec={typeSpec}
|
||||
pageProps={props}
|
||||
/>
|
||||
<>
|
||||
<RefSEO title={pageTitle} />
|
||||
|
||||
<RefSectionHandler
|
||||
sections={filteredSection}
|
||||
spec={spec}
|
||||
typeSpec={typeSpec}
|
||||
pageProps={props}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { flattenSections } from '~/lib/helpers'
|
||||
import handleRefGetStaticPaths from '~/lib/mdx/handleRefStaticPaths'
|
||||
import handleRefStaticProps from '~/lib/mdx/handleRefStaticProps'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import RefSEO from '~/components/reference/RefSEO'
|
||||
const sections = flattenSections(clientLibsCommonSections)
|
||||
|
||||
export default function JSReference(props) {
|
||||
@@ -15,13 +15,21 @@ export default function JSReference(props) {
|
||||
const slug = router.query.slug[0]
|
||||
const filteredSection = sections.filter((section) => section.id === slug)
|
||||
|
||||
const pageTitle = filteredSection[0]?.title
|
||||
? `${filteredSection[0]?.title} | Supabase`
|
||||
: 'Supabase'
|
||||
|
||||
return (
|
||||
<RefSectionHandler
|
||||
sections={filteredSection}
|
||||
spec={spec}
|
||||
typeSpec={typeSpec}
|
||||
pageProps={props}
|
||||
/>
|
||||
<>
|
||||
<RefSEO title={pageTitle} />
|
||||
|
||||
<RefSectionHandler
|
||||
sections={filteredSection}
|
||||
spec={spec}
|
||||
typeSpec={typeSpec}
|
||||
pageProps={props}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 352 KiB |
@@ -1,10 +1,9 @@
|
||||
<svg width="71" height="55" viewBox="0 0 71 55" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z" fill="#5865F2"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="71" height="55" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="75px" height="59px" viewBox="0 0 75 59" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>discord-icon</title>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="discord-icon" transform="translate(2.000000, 2.411745)" fill-rule="nonzero" stroke="#636262" stroke-width="3">
|
||||
<path d="M60.1044999,4.48605546 C55.5791999,2.40965546 50.7264999,0.879855461 45.6526999,0.00367546142 C45.5602999,-0.0132345386 45.4679999,0.0290244614 45.4203999,0.113544461 C44.7962999,1.22355546 44.1049999,2.67165546 43.6208999,3.80985546 C38.1636999,2.99285546 32.7344999,2.99285546 27.3891999,3.80985546 C26.9049999,2.64635546 26.1885999,1.22355546 25.5616999,0.113544461 C25.5140999,0.0318444614 25.4217999,-0.0104145386 25.3293999,0.00367546142 C20.2583999,0.877055461 15.4056999,2.40685546 10.8775999,4.48605546 C10.8383999,4.50295546 10.8047999,4.53115546 10.7824999,4.56775546 C1.57794989,18.3191555 -0.94356111,31.7325555 0.29340789,44.9796555 C0.29900489,45.0444555 0.33538589,45.1064555 0.38576089,45.1458555 C6.45865989,49.6056555 12.3412999,52.3131555 18.1146999,54.1077555 C18.2070999,54.1359555 18.3049999,54.1021555 18.3637999,54.0260555 C19.7294999,52.1610555 20.9468999,50.1945555 21.9906999,48.1265555 C22.0522999,48.0054555 21.9934999,47.8617555 21.8675999,47.8138555 C19.9365999,47.0813555 18.0978999,46.1882555 16.3291999,45.1740555 C16.1892999,45.0923555 16.1780999,44.8922555 16.3067999,44.7964555 C16.6789999,44.5175555 17.0512999,44.2273555 17.4066999,43.9343555 C17.4709999,43.8808555 17.5605999,43.8695555 17.6361999,43.9033555 C29.2557999,49.2084555 41.8353999,49.2084555 53.3178999,43.9033555 C53.3934999,43.8667555 53.4830999,43.8780555 53.5501999,43.9315555 C53.9056999,44.2245555 54.2778999,44.5175555 54.6528999,44.7964555 C54.7815999,44.8922555 54.7731999,45.0923555 54.6332999,45.1740555 C52.8645999,46.2079555 51.0258999,47.0813555 49.0920999,47.8110555 C48.9661999,47.8589555 48.9101999,48.0054555 48.9717999,48.1265555 C50.0379999,50.1916555 51.2553999,52.1581555 52.5958999,54.0232555 C52.6518999,54.1021555 52.7525999,54.1359555 52.8449999,54.1077555 C58.6463999,52.3131555 64.5289999,49.6056555 70.6018999,45.1458555 C70.6550999,45.1064555 70.6886999,45.0472555 70.6942999,44.9824555 C72.1746999,29.6673555 68.2146999,16.3639555 60.1967999,4.57055546 C60.1771999,4.53115546 60.1436999,4.50295546 60.1044999,4.48605546 Z M23.7258999,36.9135555 C20.2275999,36.9135555 17.3450999,33.7018555 17.3450999,29.7575555 C17.3450999,25.8132555 20.1716999,22.6015555 23.7258999,22.6015555 C27.3079999,22.6015555 30.1625999,25.8414555 30.1065999,29.7575555 C30.1065999,33.7018555 27.2799999,36.9135555 23.7258999,36.9135555 Z M47.3177999,36.9135555 C43.8195999,36.9135555 40.9370999,33.7018555 40.9370999,29.7575555 C40.9370999,25.8132555 43.7635999,22.6015555 47.3177999,22.6015555 C50.8999999,22.6015555 53.7544999,25.8414555 53.6985999,29.7575555 C53.6985999,33.7018555 50.8999999,36.9135555 47.3177999,36.9135555 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
@@ -1,22 +1,24 @@
|
||||
const fs = require('fs')
|
||||
const crypto = require('crypto')
|
||||
const path = require('path')
|
||||
const matter = require('gray-matter')
|
||||
const dotenv = require('dotenv')
|
||||
const algoliasearch = require('algoliasearch/lite')
|
||||
const { isEmpty } = require('lodash')
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
import dotenv from 'dotenv'
|
||||
import algoliasearch from 'algoliasearch/lite'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
// [Joshen] We initially thought of building out our search using Algolia directly,
|
||||
// but eventually decided to just use DocSearch since it provides us a UI and some
|
||||
// configurations out of the box (e.g Doc hierarchy).
|
||||
// search objects
|
||||
import { generateClientLibSearchObjects } from './files/client-libs'
|
||||
import { generateAPISearchObjects } from './files/api'
|
||||
import { generateCLISearchObjects } from './files/cli'
|
||||
|
||||
// As such when creating the search object, the properties that are specifically read
|
||||
const cliObjects = generateCLISearchObjects()
|
||||
const apiObjects = generateAPISearchObjects()
|
||||
const clientLibSearchObjects = generateClientLibSearchObjects()
|
||||
|
||||
// @ts-ignore
|
||||
// The properties of the searchObject that are specifically read
|
||||
// by DocSearch are "type" and "hierarchy". The rest, though saved into Algolia (which we
|
||||
// can potentially use to craft more nuanced search experiences) are not used by DocSearch
|
||||
|
||||
// Note that we'll need to do a general clean up of the files in the docs
|
||||
// A lot of them are not even linked to within the docs site, so just need to
|
||||
// double check if they can be removed or if we want them in the side bars.
|
||||
const ignoredFiles = [
|
||||
'pages/404.mdx',
|
||||
'pages/faq.mdx',
|
||||
@@ -30,18 +32,9 @@ const ignoredFiles = [
|
||||
'pages/handbook/supasquad.mdx',
|
||||
]
|
||||
|
||||
const nameMap = {
|
||||
api: 'Management API',
|
||||
cli: 'Supabase CLI',
|
||||
auth: 'Auth Server',
|
||||
storage: 'Storage Server',
|
||||
postgres: 'Postgres',
|
||||
dart: 'Supabase Flutter Library',
|
||||
javascript: 'Supabase JavaScript Library',
|
||||
}
|
||||
|
||||
async function walk(dir) {
|
||||
let files = await fs.promises.readdir(dir)
|
||||
//@ts-ignore
|
||||
files = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const filePath = path.join(dir, file)
|
||||
@@ -66,15 +59,6 @@ async function walk(dir) {
|
||||
|
||||
console.log('Preparing docs indexing for Algolia')
|
||||
|
||||
function extractMeta(mdx) {
|
||||
const regex = /^export\sconst\smeta\s=\s(\{[\w\s':,]*\})/gm
|
||||
const match = regex.exec(mdx)
|
||||
if (match) {
|
||||
return JSON.parse(match[1])
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const indexName = process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME
|
||||
const client = algoliasearch(
|
||||
@@ -83,11 +67,10 @@ async function walk(dir) {
|
||||
)
|
||||
const index = client.initIndex(indexName)
|
||||
|
||||
const referencePages = await walk('docs')
|
||||
const guidePages = (await walk('pages')).filter((slug) => !ignoredFiles.includes(slug))
|
||||
const allPages = guidePages.concat(referencePages)
|
||||
|
||||
const searchObjects = allPages
|
||||
// generate search objects for mdx guide pages
|
||||
const guidePagesearchObjects = guidePages
|
||||
.map((slug) => {
|
||||
let id, title, description
|
||||
const fileContents = fs.readFileSync(slug, 'utf8')
|
||||
@@ -118,6 +101,7 @@ async function walk(dir) {
|
||||
.replace('pages', '')
|
||||
.replace(/\.mdx$/, '')
|
||||
const source = slug.includes('/reference') ? 'reference' : 'guide'
|
||||
|
||||
const object = {
|
||||
// For Algolia
|
||||
objectID: crypto.randomUUID(),
|
||||
@@ -126,7 +110,8 @@ async function walk(dir) {
|
||||
description,
|
||||
url,
|
||||
source,
|
||||
pageContent: content,
|
||||
//pageContent: content,
|
||||
pageContent: '',
|
||||
category: undefined,
|
||||
version: undefined,
|
||||
|
||||
@@ -143,44 +128,26 @@ async function walk(dir) {
|
||||
},
|
||||
}
|
||||
|
||||
if (slug.includes('/reference')) {
|
||||
const urlSegments = url.split('/')
|
||||
const category = urlSegments[urlSegments.indexOf('reference') + 1]
|
||||
const version = urlSegments[urlSegments.indexOf('reference') + 2] || ''
|
||||
|
||||
const hasVersion = /v[0-9]+/.test(version)
|
||||
if (hasVersion) {
|
||||
object.version = version
|
||||
object.type = title === nameMap[category] ? 'lvl2' : 'lvl3'
|
||||
object.hierarchy.lvl1 = nameMap[category]
|
||||
object.hierarchy.lvl2 = version
|
||||
object.hierarchy.lvl3 = title
|
||||
} else {
|
||||
const page = urlSegments[urlSegments.indexOf('reference') + 2]
|
||||
if (page === 'generated') {
|
||||
} else {
|
||||
if (page !== undefined) {
|
||||
object.type = 'lvl2'
|
||||
object.hierarchy.lvl1 = nameMap[category]
|
||||
object.hierarchy.lvl2 = title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object.category = category ? category.replace(/\.mdx$/, '') : undefined
|
||||
object.hierarchy.lvl0 = 'References'
|
||||
}
|
||||
|
||||
return object
|
||||
})
|
||||
// Some of the reference generated files come with an 'index' page that we can ignore
|
||||
.filter((object) => !object.url.endsWith('/index'))
|
||||
.filter((object) => !object.url.endsWith('/.gitkeep'))
|
||||
|
||||
const combinedSearchObjects = guidePagesearchObjects.concat(
|
||||
clientLibSearchObjects,
|
||||
apiObjects,
|
||||
cliObjects
|
||||
)
|
||||
|
||||
//@ts-ignore
|
||||
await index.clearObjects()
|
||||
console.log(`Successfully cleared records from ${indexName}`)
|
||||
|
||||
const algoliaResponse = await index.saveObjects(searchObjects)
|
||||
//@ts-ignore
|
||||
const algoliaResponse = await index.saveObjects(combinedSearchObjects)
|
||||
|
||||
//@ts-ignore
|
||||
console.log(`Successfully saved ${algoliaResponse.objectIDs.length} records into ${indexName}.`)
|
||||
} catch (error) {
|
||||
console.log('Error:', error)
|
||||
@@ -0,0 +1,43 @@
|
||||
import crypto from 'crypto'
|
||||
import { flattenSections } from '../../lib/helpers'
|
||||
|
||||
import apiCommonSections from '~/../../spec/common-api-sections.json'
|
||||
import specFile from '~/../../spec/transforms/api_v0_openapi_deparsed.json'
|
||||
import { gen_v3 } from '../../lib/refGenerator/helpers'
|
||||
|
||||
// @ts-ignore
|
||||
const generatedSpec = gen_v3(specFile, 'wat', { apiUrl: 'apiv0' })
|
||||
const sections = flattenSections(apiCommonSections)
|
||||
|
||||
export function generateAPISearchObjects() {
|
||||
let searchObjects = []
|
||||
|
||||
//@ts-ignore
|
||||
sections.map((section) => {
|
||||
const object = searchObjects.push({
|
||||
objectID: crypto.randomUUID(),
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
// @ts-ignore
|
||||
description: generatedSpec.operations.find((item) => item.operationId === section.id)
|
||||
?.summary,
|
||||
url: `/reference/api/${section.slug}`,
|
||||
source: 'reference',
|
||||
pageContent: '',
|
||||
category: section.product,
|
||||
version: '',
|
||||
type: 'lvl2',
|
||||
hierarchy: {
|
||||
lvl0: 'References',
|
||||
lvl1: 'Management API',
|
||||
lvl2: section.title,
|
||||
lvl3: null,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
})
|
||||
return object
|
||||
})
|
||||
return searchObjects
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs'
|
||||
import { flattenSections } from '../../lib/helpers'
|
||||
import cliCommonSections from '~/../../spec/common-cli-sections.json'
|
||||
import yaml from 'js-yaml'
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
const spec = yaml.load(fs.readFileSync(`../../spec/cli_v1_commands.yaml`, 'utf8'))
|
||||
|
||||
const commonSections = flattenSections(cliCommonSections)
|
||||
|
||||
export function generateCLISearchObjects() {
|
||||
let searchObjects = []
|
||||
|
||||
//@ts-ignore
|
||||
spec.commands.map((section) => {
|
||||
const object = searchObjects.push({
|
||||
objectID: crypto.randomUUID(),
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
// @ts-ignore
|
||||
description: section.description?.substr(0, section.description.indexOf('\n')),
|
||||
url: `/reference/cli/${commonSections.find((item) => item.id === section.id)?.slug}`,
|
||||
source: 'reference',
|
||||
pageContent: '',
|
||||
category: section.product,
|
||||
version: '',
|
||||
type: 'lvl2',
|
||||
hierarchy: {
|
||||
lvl0: 'References',
|
||||
lvl1: 'Supabase CLI',
|
||||
lvl2: section.title,
|
||||
lvl3: null,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
})
|
||||
return object
|
||||
})
|
||||
return searchObjects
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto'
|
||||
import yaml from 'js-yaml'
|
||||
import { flattenSections } from '../../lib/helpers'
|
||||
import { nameMap } from '../helpers'
|
||||
|
||||
import commonLibSections from '~/../../spec/common-client-libs-sections.json'
|
||||
|
||||
const clientLibFiles = [
|
||||
{ fileName: 'supabase_js_v2', label: 'javascript', version: 'v2', versionSlug: false },
|
||||
{ fileName: 'supabase_js_v1', label: 'javascript', version: 'v1', versionSlug: true },
|
||||
{ fileName: 'supabase_dart_v1', label: 'dart', version: 'v1', versionSlug: false },
|
||||
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
|
||||
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
|
||||
]
|
||||
|
||||
const flatCommonLibSections = flattenSections(commonLibSections)
|
||||
|
||||
export function generateClientLibSearchObjects() {
|
||||
// loop through each spec file, find the correspending entry in the common file and grab the title / description / slug
|
||||
let clientLibSearchObjects = []
|
||||
|
||||
clientLibFiles.map((file) => {
|
||||
const specs = yaml.load(fs.readFileSync(`../../spec/${file.fileName}.yml`, 'utf8'))
|
||||
|
||||
//take each function id, find it in the commonLibSections file and return { id, title, slug, description, }
|
||||
//@ts-ignore
|
||||
specs.functions.map((fn) => {
|
||||
const item = flatCommonLibSections.find((section) => section.id === fn.id)
|
||||
if (item) {
|
||||
const object = clientLibSearchObjects.push({
|
||||
objectID: crypto.randomUUID(),
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
description: item.title,
|
||||
url: `/reference/${file.label}/${file.versionSlug ? file.version + '/' : ''}${item.slug}`,
|
||||
source: 'reference',
|
||||
pageContent: '',
|
||||
category: item.product,
|
||||
version: file.version,
|
||||
type: 'lvl2',
|
||||
hierarchy: {
|
||||
lvl0: 'References',
|
||||
lvl1: `${nameMap[file.label]} ${file.version}`,
|
||||
lvl2: item.title,
|
||||
lvl3: file.version,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
})
|
||||
return object
|
||||
}
|
||||
})
|
||||
})
|
||||
return clientLibSearchObjects
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export const nameMap = {
|
||||
api: 'Management API',
|
||||
cli: 'Supabase CLI',
|
||||
auth: 'Auth Server',
|
||||
storage: 'Storage Server',
|
||||
postgres: 'Postgres',
|
||||
dart: 'Supabase Flutter Library',
|
||||
javascript: 'Supabase JavaScript Library',
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// generator files need their own tsconfig.json because they don't like "module": "esnext" setting that nextjs requires in the main tsconfig
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"noImplicitAny": false,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -81,8 +81,7 @@
|
||||
--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);
|
||||
--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;
|
||||
@@ -122,8 +121,7 @@ body {
|
||||
--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);
|
||||
--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;
|
||||
@@ -429,10 +427,7 @@ body {
|
||||
}
|
||||
// Title typography
|
||||
@at-root .aa-SourceHeaderTitle {
|
||||
background: rgba(
|
||||
var(--aa-background-color-rgb),
|
||||
var(--aa-background-color-alpha)
|
||||
);
|
||||
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;
|
||||
@@ -464,8 +459,7 @@ body {
|
||||
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: 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;
|
||||
@@ -502,10 +496,7 @@ body {
|
||||
// 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)
|
||||
);
|
||||
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));
|
||||
@@ -658,10 +649,7 @@ body {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@at-root .aa-ItemContentTag {
|
||||
background-color: rgba(
|
||||
var(--aa-primary-color-rgb),
|
||||
var(--aa-primary-color-alpha)
|
||||
);
|
||||
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;
|
||||
@@ -754,10 +742,7 @@ body {
|
||||
// 8. Panel Footer
|
||||
//----------------
|
||||
.aa-PanelFooter {
|
||||
background-color: rgba(
|
||||
var(--aa-background-color-rgb),
|
||||
var(--aa-background-color-alpha)
|
||||
);
|
||||
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;
|
||||
@@ -847,10 +832,7 @@ body {
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 1px
|
||||
rgba(
|
||||
var(--aa-panel-border-color-rgb),
|
||||
var(--aa-panel-border-color-alpha)
|
||||
);
|
||||
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -969,7 +951,11 @@ body {
|
||||
}
|
||||
|
||||
// search bar
|
||||
input[type='search'].DocSearch-Input {
|
||||
background-color: var(--colors-scale1);
|
||||
}
|
||||
|
||||
.dark .DocSearch-Input {
|
||||
background-color: var(--colors-scale2);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Cancel {
|
||||
button[type='reset'].DocSearch-Cancel {
|
||||
appearance: none;
|
||||
flex: none;
|
||||
font-size: 0;
|
||||
@@ -202,7 +202,7 @@
|
||||
@apply ring-scale-300 bg-white shadow-sm ring-1 dark:bg-slate-700 dark:ring-0;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Parent .DocSearch-Hit-icon {
|
||||
.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");
|
||||
}
|
||||
@@ -289,7 +289,11 @@
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-action path {
|
||||
stroke: white;
|
||||
stroke: #777;
|
||||
|
||||
.dark & {
|
||||
stroke: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Hit > a {
|
||||
@@ -392,7 +396,7 @@
|
||||
|
||||
.DocSearch-StartScreen {
|
||||
.DocSearch-Help {
|
||||
@apply text-center py-16;
|
||||
@apply text-center py-16;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,4 +531,4 @@
|
||||
|
||||
.DocSearch-Hit-action + .DocSearch-Hit-action {
|
||||
// @apply ml-3 border-l border-slate-200 pl-3 dark:border-slate-200;
|
||||
}
|
||||
}
|
||||
@@ -253,3 +253,22 @@ div[role='tablist'] {
|
||||
max-width: 620px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* sets the image in @Next/Image components
|
||||
* to respect the height of the content
|
||||
*
|
||||
*/
|
||||
.next-image--dynamic-fill {
|
||||
width: 100%;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.next-image--dynamic-fill > span {
|
||||
position: relative !important;
|
||||
}
|
||||
.next-image--dynamic-fill img {
|
||||
object-fit: contain;
|
||||
width: 100% !important;
|
||||
position: relative !important;
|
||||
height: unset !important;
|
||||
}
|
||||
@@ -66,4 +66,4 @@ We also have guides and tools for [migrating Firebase Auth to Supabase Auth](htt
|
||||
|
||||
These are by far the most complete Firebase to Postgres migration tools available anywhere on the web.
|
||||
|
||||
You can [try Supabase for free](https://app.supabase.com). If you require Enterprise level support with your project or migration, please get in touch using our [Enterprise contact form](https://supabase.com/contact/enterprise).
|
||||
You can [try Supabase for free](https://app.supabase.com). If you require Enterprise level support with your project or migration, please get in touch using our [Enterprise contact form](https://forms.supabase.com/enterprise).
|
||||
@@ -37,13 +37,13 @@ GitHub stars rocketed for our two main repos, [supabase](https://github.com/supa
|
||||
|
||||
Here are the things that survived well.
|
||||
|
||||
### Middleware: docker-compose up
|
||||
### Middleware: docker compose up
|
||||
|
||||
This was the most surprising survivor. Our middleware was served from a single Ubuntu server with 4 CPUs and 8GB of RAM. This server was running our middleware using `docker-compose up`:
|
||||
This was the most surprising survivor. Our middleware was served from a single Ubuntu server with 4 CPUs and 8GB of RAM. This server was running our middleware using `docker compose up`:
|
||||
|
||||

|
||||

|
||||
|
||||
In case you're wondering why any sane company would use that in production, it's because we weren't planning to launch - the HackerNews post was created by an early GitHub follower, while we were alpha testing, and it was too scary to migrate the middleware while it was servicing the thundering herd. All Supabase projects use the same middleware stack (sans docker-compose), so I guess this counts as as a successful load test.
|
||||
In case you're wondering why any sane company would use that in production, it's because we weren't planning to launch - the HackerNews post was created by an early GitHub follower, while we were alpha testing, and it was too scary to migrate the middleware while it was servicing the thundering herd. All Supabase projects use the same middleware stack (without docker compose), so I guess this counts as a successful load test.
|
||||
|
||||
We have since migrated our middleware to multiple ECS clusters, globally load-balanced using AWS's [Global Accelerator](https://aws.amazon.com/global-accelerator/).
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ Also check `kong.yml` inside the `kong` directory where you'll see how all the s
|
||||
Once you're all set, you can start the stack by running:
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Head over to the [Self Hosting Docs](/docs/guides/self-hosting) for a more complete walk through, it also includes several [one-click deploys](/docs/guides/self-hosting#one-click-deploys), so you can easily deploy into your own cloud hosting provider.
|
||||
|
||||
@@ -57,13 +57,13 @@ Ever wanted to build a Search Engine? We just released a guide which shows you h
|
||||
### OAuth Guides
|
||||
|
||||
We released step-by-step guides to help you set up OAuth with
|
||||
[Apple](/docs/guides/auth/auth-apple),
|
||||
[Bitbucket](/docs/guides/auth/auth-bitbucket),
|
||||
[Facebook](/docs/guides/auth/auth-facebook),
|
||||
[GitHub](/docs/guides/auth/auth-github),
|
||||
[GitLab](/docs/guides/auth/auth-gitlab),
|
||||
[Google](/docs/guides/auth/auth-google), and
|
||||
[Twitter](/docs/guides/auth/auth-twitter).
|
||||
[Apple](/docs/guides/auth/social-login/auth-apple),
|
||||
[Bitbucket](/docs/guides/auth/social-login/auth-bitbucket),
|
||||
[Facebook](/docs/guides/auth/social-login/auth-facebook),
|
||||
[GitHub](/docs/guides/auth/social-login/auth-github),
|
||||
[GitLab](/docs/guides/auth/social-login/auth-gitlab),
|
||||
[Google](/docs/guides/auth/social-login/auth-google), and
|
||||
[Twitter](/docs/guides/auth/social-login/auth-twitter).
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ Wednesday 28 July 2021.[^1]
|
||||
|
||||
### What?
|
||||
|
||||
What's cooler than Arnie in the 90's? Passwordless logins. We've [already](/docs/guides/auth/auth-apple) [built](/docs/guides/auth/auth-bitbucket) [support](/docs/guides/auth/auth-discord) [for](/docs/guides/auth/auth-facebook) [eight](/docs/guides/auth/auth-github) [different](/docs/guides/auth/auth-gitlab) [Oauth](/docs/guides/auth/auth-google) [providers](/docs/guides/auth/auth-twitter), so it's anyone's guess what can happen in Supabase Auth Episode II.
|
||||
What's cooler than Arnie in the 90's? Passwordless logins. We've [already](/docs/guides/auth/social-login/auth-apple) [built](/docs/guides/auth/social-login/auth-bitbucket) [support](/docs/guides/auth/social-login/auth-discord) [for](/docs/guides/auth/social-login/auth-facebook) [eight](/docs/guides/auth/social-login/auth-github) [different](/docs/guides/auth/social-login/auth-gitlab) [Oauth](/docs/guides/auth/social-login/auth-google) [providers](/docs/guides/auth/social-login/auth-twitter), so it's anyone's guess what can happen in Supabase Auth Episode II.
|
||||
|
||||
### Where?
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ with either an OTP or a phone + password combo.
|
||||
Supabase Auth supports [Twilio](https://www.twilio.com/) as an SMS provider, with more options coming soon.
|
||||
Simply plug your Twilio credentials into your Auth Settings in the Supabase Dashboard to get started.
|
||||
|
||||
Check out the [documentation](/docs/guides/auth/auth-twilio) to get started with Mobile OTPs,
|
||||
Check out the [documentation](/docs/guides/auth/phone-login/twilio) to get started with Mobile OTPs,
|
||||
or watch the [Youtube guide](https://youtu.be/akScoPO01bc).
|
||||
|
||||
<iframe
|
||||
@@ -74,7 +74,7 @@ Auth and will be offering that as an option soon.
|
||||
The community has contributed tons of OAuth providers, and today we're announcing two more.
|
||||
|
||||
Shoutout to [@ph1p](https://github.com/ph1p) who contributed [Twitch](/docs/guides/auth/auth-twitch) as our latest OAuth provider!
|
||||
The Supabase team added [Discord](/docs/guides/auth/auth-discord) last month, bringing the total OAuth Providers to ten.
|
||||
The Supabase team added [Discord](/docs/guides/auth/social-login/auth-discord) last month, bringing the total OAuth Providers to ten.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ The community has been sieving through a slew of bad puns and retro memes to dis
|
||||
|
||||
## Auth v2 with Phone Auth
|
||||
|
||||
Your users can now log in with SMS based mobile auth! We have a Twilio integration ([Guide Here](/docs/guides/auth/auth-twilio)) and will be adding more providers soon.
|
||||
Your users can now log in with SMS based mobile auth! We have a Twilio integration ([Guide Here](/docs/guides/auth/phone-login/twilio)) and will be adding more providers soon.
|
||||
|
||||
Other Auth updating include, Twitch logins, and the ability to generate invite, recovery, confirmation, and magic links via the API,
|
||||
for people who want more control over the email templating flow. Read the [blog post here](/blog/supabase-auth-passwordless-sms-login).
|
||||
|
||||
@@ -50,7 +50,7 @@ It's not in production yet, but we're making the repo public for comments using
|
||||
|
||||
## Custom SMS templates
|
||||
|
||||
If you're using SMS login in [Auth v2](/blog/2021/07/28/supabase-auth-passwordless-sms-login), you can now customize the SMS which is sent to your users. Read more in the [docs](/docs/guides/auth/auth-twilio#sms-custom-template).
|
||||
If you're using SMS login in [Auth v2](/blog/2021/07/28/supabase-auth-passwordless-sms-login), you can now customize the SMS which is sent to your users. Read more in the [docs](/docs/guides/auth/phone-login/twilio#sms-custom-template).
|
||||
|
||||

|
||||
|
||||
|
||||