Merge branch 'master' into chore/switch-to-slash-dashboard

This commit is contained in:
Alaister Young committed 2023-05-23 17:11:44 +10:00
commit 4bdef60e09
312 files changed
+5584 -2446

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
name: Docs Tests
on:
pull_request:
branches: ['master']
paths:
- 'apps/docs/**/*.{ts,tsx}'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install deps
run: npm ci
- name: Run tests
run: npm run test:docs
+13 -2
View File
@@ -9,7 +9,12 @@ on:
- 'supabase/migrations/**'
- 'apps/docs/**'
- 'spec/**'
workflow_dispatch: {}
workflow_dispatch:
inputs:
refresh:
description: 'Refresh all pages'
required: false
type: boolean
schedule:
- cron: '0 0 * * *'
@@ -46,6 +51,12 @@ jobs:
- name: Run migrations
run: npx supabase db push
- name: Update embeddings
working-directory: ./apps/docs
if: ${{ !inputs.refresh }}
run: npm run embeddings
- name: Refresh embeddings
working-directory: ./apps/docs
run: npm run embeddings
if: ${{ inputs.refresh }}
run: npm run embeddings:refresh
+30
View File
@@ -0,0 +1,30 @@
name: UI Tests
on:
pull_request:
branches: [master]
paths:
- 'packages/ui/**'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install deps
run: npm ci
- name: Run tests
run: npm run test:ui
@@ -0,0 +1,96 @@
import { getAnchor, removeAnchor } from './CustomHTMLElements.utils'
describe('CustomHTMLElementsUtils', () => {
describe('getAnchor', () => {
describe('when value is an object', () => {
it('returns slugified version of props.children', () => {
const value = {
props: {
children: 'Full Text Search',
},
}
const result = getAnchor(value)
expect(result).toStrictEqual('full-text-search')
})
})
describe('when value is an array', () => {
describe('when custom anchor exists', () => {
it('returns inner slug', () => {
const value = ['Proximity: <->', '[#proximity]']
const result = getAnchor(value)
expect(result).toStrictEqual('proximity')
})
it('trims whitespace', () => {
const value = ['Proximity: <->', ' [#proximity] ']
const result = getAnchor(value)
expect(result).toStrictEqual('proximity')
})
})
it('returns concatenated slug of elements', () => {
const value = ['Full', 'Text', 'Search']
const result = getAnchor(value)
expect(result).toStrictEqual('full-text-search')
})
it('trims whitespace', () => {
const value = [' Full ', ' Text ', ' Search ']
const result = getAnchor(value)
expect(result).toStrictEqual('full-text-search')
})
it('removes special characters', () => {
const value = ['function()']
const result = getAnchor(value)
expect(result).toStrictEqual('function')
})
})
describe('when value is a string', () => {
it('returns slugified version of string', () => {
const value = 'My (Very) Awesome Heading'
const result = getAnchor(value)
expect(result).toStrictEqual('my-very-awesome-heading')
})
})
})
describe('removeAnchor', () => {
describe('when value is an array', () => {
it('filters out custom anchor elements', () => {
const value = ['My (Very) Awesome Heading', '[#my-custom-heading]']
const result = removeAnchor(value)
expect(result).toStrictEqual(['My (Very) Awesome Heading'])
})
})
describe('when value is a string', () => {
it('strips out custom anchor string', () => {
const value = 'My (Very) Awesome Heading [#my-custom-heading]'
const result = removeAnchor(value)
// Original implementation didn't trim the resulting string - not sure if it really matters
expect(result).toStrictEqual('My (Very) Awesome Heading ')
})
})
})
})
@@ -2,53 +2,60 @@
export const getAnchor = (text: any): string | undefined => {
if (typeof text === 'object') {
if (Array.isArray(text)) {
const customAnchor = text.find(
(x) => typeof x === 'string' && x.includes('[#') && x.endsWith(']')
)
if (customAnchor !== undefined) return customAnchor.slice(2, customAnchor.indexOf(']'))
const customAnchor = text.find((x) => typeof x === 'string' && hasCustomAnchor(x))
if (customAnchor !== undefined) {
return parseCustomAnchor(customAnchor)
}
const formattedText = text
.map((x) => {
if (typeof x !== 'string') return x.props.children
else return x.trim()
if (typeof x !== 'string') {
return x.props.children
}
return x.trim()
})
.map((x) => {
if (typeof x !== 'string') return x
else
if (typeof x !== 'string') {
return x
.toLowerCase()
.replace(/[^a-z0-9- ]/g, '')
.replace(/[ ]/g, '-')
}
return slugify(x)
})
return formattedText.join('-').toLowerCase()
} else {
const anchor = text.props.children
if (typeof anchor === 'string') {
return anchor
.toLowerCase()
.replace(/[^a-z0-9- ]/g, '')
.replace(/[ ]/g, '-')
return slugify(anchor)
}
return anchor
}
} else if (typeof text === 'string') {
if (text.includes('[#') && text.endsWith(']')) {
return text.slice(text.indexOf('[#') + 2, text.indexOf(']'))
} else {
return text
.toLowerCase()
.replace(/[^a-z0-9- ]/g, '')
.replace(/[ ]/g, '-')
if (hasCustomAnchor(text)) {
return parseCustomAnchor(text)
}
return slugify(text)
} else {
return undefined
}
}
const hasCustomAnchor = (value: string): boolean => value.includes('[#') && value.includes(']')
const parseCustomAnchor = (value: string): string =>
value.slice(value.indexOf('[#') + 2, value.indexOf(']'))
const slugify = (value: string): string =>
value
.toLowerCase()
.trim()
.replace(/[^a-z0-9- ]/g, '')
.replace(/[ ]/g, '-')
export const removeAnchor = (text: any) => {
if (typeof text === 'object' && Array.isArray(text)) {
return text.filter((x) => !(typeof x === 'string' && x.includes('[#') && x.endsWith(']')))
return text.filter((x) => !(typeof x === 'string' && hasCustomAnchor(x)))
} else if (typeof text === 'string') {
if (text.indexOf('[#') > 0) return text.slice(0, text.indexOf('[#'))
else return text
+497
View File
@@ -0,0 +1,497 @@
import React from 'react'
import { LazyMotion, domAnimation, m } from 'framer-motion'
const DocsCoverLogo = () => {
const pathMotionConfig = {
initial: { pathLength: 0 },
animate: { pathLength: 1 },
transition: {
duration: 1,
delay: 0.2,
ease: [0.5, 0.11, 0.13, 1],
},
}
const logoMotionConfig = {
initial: { fillOpacity: 0 },
animate: { fillOpacity: 1 },
transition: {
duration: 1,
delay: 0.5,
ease: [0.25, 0.25, 0, 1],
},
}
return (
<div className="w-[60px] md:w-[150px] [&_svg]">
<LazyMotion features={domAnimation}>
<m.svg
width="100%"
height="100%"
viewBox="0 0 250 228"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M124.951 5.03906V224.602"
stroke="url(#paint0_radial_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M125.25 191.383C167.828 191.383 202.344 156.867 202.344 114.289C202.344 71.7115 167.828 37.1953 125.25 37.1953C82.6724 37.1953 48.1562 71.7115 48.1562 114.289C48.1562 156.867 82.6724 191.383 125.25 191.383Z"
stroke="var(--colors-scale11)"
strokeOpacity="0.1"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M33.7782 58.8377C48.6666 58.8377 60.7361 46.7682 60.7361 31.8798C60.7361 16.9913 48.6666 4.92188 33.7782 4.92188C18.8898 4.92188 6.82031 16.9913 6.82031 31.8798C6.82031 46.7682 18.8898 58.8377 33.7782 58.8377Z"
stroke="url(#paint1_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M33.7782 223.664C48.6666 223.664 60.7361 211.594 60.7361 196.706C60.7361 181.817 48.6666 169.748 33.7782 169.748C18.8898 169.748 6.82031 181.817 6.82031 196.706C6.82031 211.594 18.8898 223.664 33.7782 223.664Z"
stroke="url(#paint2_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M216.724 58.8377C231.612 58.8377 243.681 46.7682 243.681 31.8798C243.681 16.9913 231.612 4.92188 216.724 4.92188C201.835 4.92188 189.766 16.9913 189.766 31.8798C189.766 46.7682 201.835 58.8377 216.724 58.8377Z"
stroke="url(#paint3_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M216.724 223.664C231.612 223.664 243.681 211.594 243.681 196.706C243.681 181.817 231.612 169.748 216.724 169.748C201.835 169.748 189.766 181.817 189.766 196.706C189.766 211.594 201.835 223.664 216.724 223.664Z"
stroke="url(#paint4_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
fillRule="evenodd"
clipRule="evenodd"
d="M209.29 226.982H40.6907C18.7665 226.982 1 209.51 1 187.949V40.0333C1 18.4722 18.7665 1 40.6907 1H209.29C231.214 1 248.981 18.4722 248.981 40.0333V187.969C248.981 209.51 231.214 226.982 209.29 226.982Z"
stroke="url(#paint5_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
fillRule="evenodd"
clipRule="evenodd"
d="M216.724 223.903H33.7779C18.719 223.903 6.5 211.684 6.5 196.625V31.9576C6.5 16.8987 18.719 4.67969 33.7779 4.67969H216.724C231.782 4.67969 244.001 16.8987 244.001 31.9576V196.625C244.001 211.704 231.802 223.903 216.724 223.903Z"
stroke="url(#paint6_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M60.7578 30.9199V197.027"
stroke="url(#paint7_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M125.252 5.01953V223.562"
stroke="url(#paint8_radial_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M149.809 4.83984L13.5996 178.866"
stroke="url(#paint9_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M239.679 46.457L99.5703 223.643"
stroke="url(#paint10_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M189.766 30.9199V197.027"
stroke="url(#paint11_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M218.024 58.7344H32.498"
stroke="url(#paint12_radial_0_1)"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M6.76172 95.4141H243.743"
stroke="url(#paint13_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M6.76172 131.869H243.743"
stroke="url(#paint14_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M218.024 169.846H32.498"
stroke="var(--colors-scale11)"
strokeOpacity="0.1"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M126.559 4.6867L126.559 224.254"
stroke="url(#paint15_linear_0_1)"
strokeOpacity="0.5"
strokeWidth="0.7"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
<g filter="url(#filter0_d_0_1)">
<m.path
initial={logoMotionConfig.initial}
animate={logoMotionConfig.animate}
transition={logoMotionConfig.transition as any}
d="M125.751 174.003C125.833 179.464 134.091 181.807 137.49 177.53L188.786 112.985C194.84 105.367 189.41 94.1317 179.674 94.1317H126.964L127.046 52.5389C126.964 47.078 119.027 44.7344 115.628 49.0116L65.5801 113.556C59.5255 121.175 64.9558 132.41 74.6925 132.41H125.161L125.751 174.003Z"
fill="url(#paint16_linear_0_1)"
fillOpacity="0.79"
shapeRendering="crispEdges"
/>
<m.path
initial={pathMotionConfig.initial}
animate={pathMotionConfig.animate}
transition={pathMotionConfig.transition as any}
d="M126.964 94.6797L126.416 94.1306L126.498 52.5425C126.459 50.117 124.68 48.3327 122.422 47.6478C120.162 46.9621 117.586 47.432 116.059 49.3499C116.059 49.3508 116.058 49.3516 116.057 49.3525L66.0131 113.892L66.0091 113.897C60.2405 121.156 65.4135 131.862 74.6925 131.862H125.161L125.709 132.402L126.299 173.995L126.299 173.995M126.964 94.6797L125.751 174.003L126.299 173.995M126.964 94.6797H179.674C188.953 94.6797 194.126 105.386 188.357 112.644L137.061 177.189C135.539 179.104 132.883 179.583 130.531 178.892C128.18 178.202 126.336 176.41 126.299 173.995M126.964 94.6797L126.299 173.995"
stroke="url(#paint17_linear_0_1)"
strokeWidth="1.09591"
strokeMiterlimit="10"
strokeLinejoin="bevel"
shapeRendering="crispEdges"
/>
</g>
<defs>
<filter
id="filter0_d_0_1"
x="58.6515"
y="46.8027"
width="137.062"
height="141.703"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4.38364" />
<feGaussianBlur stdDeviation="2.19182" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_1" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_0_1"
result="shape"
/>
</filter>
<radialGradient
id="paint0_radial_0_1"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(125.451 114.82) rotate(90) scale(109.781 0.5)"
>
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</radialGradient>
<linearGradient
id="paint1_linear_0_1"
x1="38.9975"
y1="58.9958"
x2="18.999"
y2="11.9994"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint2_linear_0_1"
x1="43.9971"
y1="168.986"
x2="32.998"
y2="204.984"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint3_linear_0_1"
x1="221.943"
y1="58.9958"
x2="231.981"
y2="13.9993"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint4_linear_0_1"
x1="203.983"
y1="168.986"
x2="215.943"
y2="204.984"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint5_linear_0_1"
x1="121.402"
y1="227.645"
x2="121.402"
y2="-7.94661"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale10)" stopOpacity="0" />
<stop offset="0.489583" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint6_linear_0_1"
x1="121.814"
y1="224.545"
x2="121.814"
y2="-3.99932"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale10)" stopOpacity="0" />
<stop offset="0.489583" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint7_linear_0_1"
x1="60.7578"
y1="30.3703"
x2="60.7578"
y2="199.061"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale10)" stopOpacity="0.1" />
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.1" />
</linearGradient>
<radialGradient
id="paint8_radial_0_1"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(125.752 114.291) rotate(90) scale(109.271 0.5)"
>
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</radialGradient>
<linearGradient
id="paint9_linear_0_1"
x1="6.99856"
y1="0"
x2="200"
y2="6.31757"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale11)" stopOpacity="0.3" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint10_linear_0_1"
x1="200"
y1="100"
x2="0"
y2="200"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale11)" stopOpacity="0.3" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint11_linear_0_1"
x1="189.766"
y1="30.3703"
x2="189.766"
y2="199.061"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale10)" stopOpacity="0.1" />
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.1" />
</linearGradient>
<radialGradient
id="paint12_radial_0_1"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(125.261 59.2344) rotate(90) scale(0.5 263.749)"
>
<stop stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0" />
</radialGradient>
<linearGradient
id="paint13_linear_0_1"
x1="6.99998"
y1="94.9749"
x2="242.982"
y2="94.9749"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale10)" stopOpacity="0.5" />
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.5" />
</linearGradient>
<linearGradient
id="paint14_linear_0_1"
x1="6.99998"
y1="131.43"
x2="242.982"
y2="131.43"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale10)" stopOpacity="0.5" />
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.5" />
</linearGradient>
<linearGradient
id="paint15_linear_0_1"
x1="126.559"
y1="224.98"
x2="126.559"
y2="1.99785"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="var(--colors-scale10)" stopOpacity="0.1" />
<stop offset="0.505208" stopColor="var(--colors-scale11)" stopOpacity="0.5" />
<stop offset="1" stopColor="var(--colors-scale10)" stopOpacity="0.1" />
</linearGradient>
<linearGradient
id="paint16_linear_0_1"
x1="155.11"
y1="46.8027"
x2="126.979"
y2="206.049"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#212121" />
<stop offset="1" stopColor="white" stopOpacity="0.12" />
</linearGradient>
<linearGradient
id="paint17_linear_0_1"
x1="191.328"
y1="82.7684"
x2="103.041"
y2="175.889"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#868585" />
<stop offset="0.208333" stopColor="#838383" />
<stop offset="1" stopColor="#5A5A5A" />
</linearGradient>
</defs>
</m.svg>
</LazyMotion>
</div>
)
}
export default DocsCoverLogo
+129
View File
@@ -0,0 +1,129 @@
import Link from 'next/link'
import { IconBackground, IconPanel, IconPlay } from 'ui'
import { useBreakpoint } from 'common'
import DocsCoverLogo from './DocsCoverLogo'
const HomePageCover = (props) => {
const isXs = useBreakpoint(639)
const iconSize = isXs ? 'sm' : 'lg'
const GettingStarted = () => (
<div
className="
border border-scale-400 bg-scale-200
relative overflow-hidden
grid grid-cols-12
rounded-lg
p-5 md:p-8
"
>
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-1 md:gap-6">
<div className="md:max-w-xs xl:max-w-none">
<div className="flex items-center gap-3 mb-3">
<IconBackground>
<IconPlay className="text-brand-1100 dark:text-brand-900 w-4" />
</IconBackground>
<h3 className="text-2xl m-0">Getting Started</h3>
</div>
<p className="text-scale-1100 text-sm">
Discover how to set up a database to an app making queries in just a few minutes.
</p>
</div>
<div className="flex flex-wrap md:grid md:grid-cols-4 gap-2 sm:gap-3">
<Link href={`/guides/getting-started/quickstarts/reactjs`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="ReactJS"
icon="/docs/img/icons/react-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/nextjs`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="NextJS"
icon="/docs/img/icons/nextjs-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/redwoodjs`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="RedwoodJS"
icon="/docs/img/icons/redwoodjs-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/flutter`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="Flutter"
icon="/docs/img/icons/flutter-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/sveltekit`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="SvelteKit"
icon="/docs/img/icons/svelte-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/solidjs`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="SolidJS"
icon="/docs/img/icons/solidjs-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/vue`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="Vue"
icon="/docs/img/icons/vuejs-icon"
/>
</a>
</Link>
</div>
</div>
</div>
)
return (
<div className="w-full bg-scale-100 border-b prose dark:prose-dar max-w-none mb-16 md:mb-12 xl:mb-0">
<div className="max-w-7xl px-5 mx-auto py-8 sm:pb-16 sm:pt-12 xl:pt-16 flex flex-col xl:flex-row justify-between gap-12 xl:gap-12">
<div className="flex flex-col sm:flex-row gap-4 sm:gap-8 items-start sm:items-center w-full max-w-xl">
<DocsCoverLogo />
<div className="flex flex-col">
<h1 className="m-0 mb-3 text-2xl sm:text-3xl">{props.meta?.title}</h1>
<p className="m-0 text-scale-1100">
Learn how to get up and running with Supabase through tutorials, APIs and platform
resources.
</p>
</div>
</div>
<div className="w-full xl:max-w-[365px] -mb-40">
<GettingStarted />
</div>
</div>
</div>
)
}
export default HomePageCover
+155 -60
View File
@@ -1,68 +1,163 @@
import Image from 'next/image'
import Link from 'next/link'
import { Button, IconGitHub, IconTwitter, IconYoutube } from '~/../../packages/ui'
import footerData from '~/data/footer.json'
import { useTheme } from 'common/Providers'
import { Button } from 'ui'
import { primaryLinks, secondaryLinks } from '~/data/footer'
import { LayoutMainContent } from '../../layouts/DefaultLayout'
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 ">
<Link href="https://supabase.com/">
<a className="text-xs text-scale-900">&copy; Supabase Inc</a>
</Link>
<span className="text-xs text-scale-900">—</span>
{footerData.map((item) => (
<Link href={item.url} key={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://youtube.com/c/supabase"
target="_blank"
rel="noreferrer noopener"
>
<IconYoutube size={16} />
</Button>
<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
const Footer = () => (
<LayoutMainContent className="pt-0">
<div className="mt-16">
<ul className="flex flex-col gap-2">
{primaryLinks.map(({ url, featherIcon: Icon, icon, text, ctaLabel }) => (
<li key={url} className="flex items-center gap-1 text-xs text-scale-900">
{icon && (
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
fillRule="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"
xmlns="http://www.w3.org/2000/svg"
>
<path
d={icon}
stroke="var(--colors-scale9)"
strokeWidth={0.7}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
{Icon && <Icon width={16} height={16} />}
<p>{text}</p>
<Link href={url} passHref>
<a
className="text-brand-900 hover:underline"
target="_blank"
rel="noreferrer noopener"
>
{ctaLabel}
</a>
</Link>
</li>
))}
</ul>
</div>
<hr className="border-scale-400 my-6"></hr>
<div className="flex gap-4 items-center justify-between">
<div className="flex flex-col lg:flex-row gap-3 ">
<Link href="https://supabase.com/">
<a className="text-xs text-scale-900">&copy; Supabase Inc</a>
</Link>
<span className="text-xs text-scale-900">—</span>
{secondaryLinks.map((item) => (
<Link href={item.url} key={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://github.com/supabase/supabase"
target="_blank"
rel="noreferrer noopener"
>
<svg
width="17"
height="17"
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.66832 1.55469C4.98649 1.55469 2.00195 4.54402 2.00195 8.23269C2.00195 11.1827 3.91187 13.686 6.56108 14.5687C6.8944 14.63 7.01573 14.424 7.01573 14.2467C7.01573 14.0887 7.0104 13.668 7.00706 13.1114C5.15248 13.5147 4.76116 12.216 4.76116 12.216C4.45851 11.444 4.0212 11.2387 4.0212 11.2387C3.41589 10.8254 4.06719 10.8334 4.06719 10.8334C4.73583 10.88 5.08782 11.5214 5.08782 11.5214C5.68246 12.5414 6.64841 12.2467 7.02773 12.076C7.08906 11.6447 7.26105 11.3507 7.45171 11.184C5.97178 11.0154 4.41518 10.442 4.41518 7.88335C4.41518 7.15469 4.67517 6.55802 5.10115 6.09135C5.03248 5.92269 4.80383 5.24335 5.16648 4.32469C5.16648 4.32469 5.72645 4.14469 6.99973 5.00869C7.54355 4.86036 8.10464 4.78482 8.66832 4.78402C9.23496 4.78669 9.80494 4.86069 10.3376 5.00869C11.6102 4.14469 12.1688 4.32402 12.1688 4.32402C12.5328 5.24335 12.3035 5.92269 12.2355 6.09135C12.6621 6.55802 12.9208 7.15469 12.9208 7.88335C12.9208 10.4487 11.3615 11.0134 9.87694 11.1787C10.1163 11.3847 10.3289 11.792 10.3289 12.4154C10.3289 13.3074 10.3209 14.028 10.3209 14.2467C10.3209 14.4254 10.4409 14.6334 10.7796 14.568C12.107 14.1228 13.261 13.2716 14.0784 12.1347C14.8958 10.9979 15.3353 9.6329 15.3347 8.23269C15.3347 4.54402 12.3495 1.55469 8.66832 1.55469Z"
fill="var(--colors-scale9)"
/>
</svg>
</Button>
<Button
type="text"
as="a"
// @ts-ignore
href="https://youtube.com/c/supabase"
target="_blank"
rel="noreferrer noopener"
>
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.665 4.35595C15.481 3.66308 14.9388 3.11739 14.2505 2.93221C13.0028 2.5957 7.99964 2.5957 7.99964 2.5957C7.99964 2.5957 2.99655 2.5957 1.74883 2.93221C1.06047 3.11742 0.518326 3.66308 0.334321 4.35595C0 5.61181 0 8.23207 0 8.23207C0 8.23207 0 10.8523 0.334321 12.1082C0.518326 12.8011 1.06047 13.324 1.74883 13.5092C2.99655 13.8457 7.99964 13.8457 7.99964 13.8457C7.99964 13.8457 13.0027 13.8457 14.2505 13.5092C14.9388 13.324 15.481 12.8011 15.665 12.1082C15.9993 10.8523 15.9993 8.23207 15.9993 8.23207C15.9993 8.23207 15.9993 5.61181 15.665 4.35595ZM6.36334 10.6111V5.85307L10.545 8.23212L6.36334 10.6111Z"
fill="var(--colors-scale9)"
/>
</svg>
</Button>
<Button
type="text"
as="a"
// @ts-ignore
href="https://twitter.com/supabase"
target="_blank"
rel="noreferrer noopener"
>
<svg
width="17"
height="17"
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.52713 13.7223C10.5582 13.7223 13.3101 9.55365 13.3101 5.93898C13.3101 5.82032 13.3101 5.70232 13.3021 5.58565C13.8375 5.198 14.2996 4.71806 14.6667 4.16832C14.1675 4.38965 13.6378 4.53483 13.0955 4.59898C13.6665 4.25702 14.0939 3.71922 14.2981 3.08565C13.7612 3.40424 13.1739 3.6288 12.5615 3.74965C12.1491 3.31083 11.6035 3.02021 11.0093 2.92279C10.4151 2.82537 9.80528 2.92658 9.27438 3.21076C8.74347 3.49494 8.32106 3.94623 8.07255 4.49476C7.82403 5.04328 7.76328 5.65844 7.89969 6.24498C6.81213 6.19048 5.74819 5.90787 4.77693 5.4155C3.80567 4.92313 2.9488 4.232 2.26194 3.38698C1.91217 3.98912 1.80502 4.70193 1.96231 5.38029C2.1196 6.05866 2.52951 6.65157 3.10857 7.03832C2.67343 7.02572 2.2477 6.90865 1.86729 6.69698V6.73165C1.86755 7.36318 2.08621 7.97519 2.4862 8.46389C2.88618 8.95259 3.44287 9.28791 4.06186 9.41298C3.65909 9.52279 3.2365 9.53876 2.82658 9.45965C3.00135 10.0033 3.34168 10.4787 3.79996 10.8194C4.25824 11.1601 4.81154 11.349 5.38247 11.3597C4.81524 11.8054 4.16572 12.1349 3.47104 12.3293C2.77636 12.5238 2.05017 12.5794 1.33398 12.493C2.585 13.2959 4.04066 13.7217 5.52713 13.7197"
fill="var(--colors-scale9)"
/>
</svg>
</Button>
<Button
type="text"
as="a"
// @ts-ignore
href="https://discord.supabase.com/"
target="_blank"
rel="noreferrer noopener"
>
<svg
width="17"
height="17"
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_1182_99731)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.8781 3.13594C12.8583 2.66802 11.7648 2.32328 10.6215 2.12583C10.6006 2.12202 10.5798 2.13154 10.5691 2.15059C10.4285 2.40073 10.2727 2.72706 10.1636 2.98356C8.93387 2.79945 7.71044 2.79945 6.50592 2.98356C6.39681 2.72136 6.23538 2.40073 6.09411 2.15059C6.08339 2.13218 6.06259 2.12265 6.04176 2.12583C4.89905 2.32265 3.80554 2.66739 2.78517 3.13594C2.77633 3.13975 2.76876 3.14611 2.76374 3.15435C0.689563 6.25326 0.12136 9.276 0.400102 12.2613C0.401363 12.2759 0.409561 12.2898 0.420913 12.2987C1.78939 13.3037 3.115 13.9139 4.41599 14.3183C4.43681 14.3247 4.45887 14.317 4.47212 14.2999C4.77987 13.8796 5.0542 13.4364 5.28941 12.9704C5.3033 12.9431 5.29005 12.9107 5.26167 12.9C4.82654 12.7349 4.4122 12.5336 4.01364 12.3051C3.98212 12.2867 3.97959 12.2416 4.00859 12.22C4.09247 12.1571 4.17636 12.0917 4.25645 12.0257C4.27094 12.0136 4.29113 12.0111 4.30816 12.0187C6.92655 13.2142 9.76126 13.2142 12.3488 12.0187C12.3658 12.0105 12.386 12.013 12.4011 12.0251C12.4812 12.0911 12.5651 12.1571 12.6496 12.22C12.6786 12.2416 12.6767 12.2867 12.6452 12.3051C12.2466 12.5381 11.8323 12.7349 11.3965 12.8993C11.3681 12.9101 11.3555 12.9431 11.3694 12.9704C11.6097 13.4358 11.884 13.879 12.1861 14.2993C12.1987 14.317 12.2214 14.3247 12.2422 14.3183C13.5495 13.9139 14.8751 13.3037 16.2436 12.2987C16.2556 12.2898 16.2631 12.2765 16.2644 12.2619C16.598 8.8106 15.7056 5.81265 13.8989 3.15498C13.8944 3.14611 13.8869 3.13975 13.8781 3.13594ZM5.68043 10.4435C4.89211 10.4435 4.24257 9.71978 4.24257 8.83093C4.24257 7.94207 4.87952 7.21831 5.68043 7.21831C6.48763 7.21831 7.13089 7.94843 7.11827 8.83093C7.11827 9.71978 6.48132 10.4435 5.68043 10.4435ZM10.9967 10.4435C10.2084 10.4435 9.55884 9.71978 9.55884 8.83093C9.55884 7.94207 10.1958 7.21831 10.9967 7.21831C11.8039 7.21831 12.4471 7.94843 12.4345 8.83093C12.4345 9.71978 11.8039 10.4435 10.9967 10.4435Z"
fill="var(--colors-scale9)"
/>
</svg>
</Button>
</div>
</g>
<defs>
<clipPath id="clip0_1182_99731">
<rect
width="15.9993"
height="16"
fill="white"
transform="translate(0.333984 0.222656)"
/>
</clipPath>
</defs>
</svg>
</Button>
</div>
</div>
)
}
</LayoutMainContent>
)
export default Footer
@@ -9,8 +9,8 @@ export interface NavMenuGroup {
export interface NavMenuSection {
name: string
url?: string
items: NavMenuSection[]
url?: `/${string}`
items: Partial<NavMenuSection>[]
}
export interface References {
@@ -26,10 +26,17 @@ export interface References {
type MenuItem = {
label: string
icon?: string
href?: string
href?: `/${string}` | `https://${string}`
level?: string
hasLightIcon?: boolean
community?: boolean
}
export type HomepageMenuItems = MenuItem[][]
export type NavMenuConstant = Readonly<{
title: string
icon: string
url?: `/${string}`
items: ReadonlyArray<Partial<NavMenuSection>>
}>
@@ -14,7 +14,7 @@ const NavigationMenuHome = () => {
return (
<div className="transition-all duration-150 ease-out opacity-100 ml-0 delay-150">
<ul className="relative w-full flex flex-col gap-4">
<ul className="relative w-full flex flex-col gap-4 pb-5">
{HOMEPAGE_MENU_ITEMS.map((section, sectionIndex) => {
return (
<Fragment key={`section-container-${sectionIndex}-border`}>
@@ -24,15 +24,15 @@ export function IconMenuHome({ width = 16, height = 16 }: HomeMenuIcon) {
export function IconMenuApi({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.923 10.927A5.833 5.833 0 0 1 2.14 8m2.877-5.046A5.833 5.833 0 0 1 8 2.14m5.13 3.025c.465.84.73 1.807.73 2.835m-2.883 5.049A5.832 5.832 0 0 1 8 13.859m6.172-10.027a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM5.81 12.148a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm8.362 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM5.81 3.832a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"
stroke="currentColor"
d="M11.7118 15.4651C10.8778 15.8159 9.96154 16.0098 9 16.0098C8.03846 16.0098 7.1222 15.8159 6.28818 15.4651M2.42438 11.4156C2.14983 10.6654 2 9.85506 2 9.00977C2 8.16447 2.14983 7.35417 2.42438 6.60397M6.28818 2.55442C7.1222 2.20364 8.03846 2.00977 9 2.00977C9.96154 2.00977 10.8778 2.20364 11.7118 2.55442M15.4553 6.29795C15.8061 7.13197 16 8.04823 16 9.00977C16 9.97131 15.8061 10.8876 15.4553 11.7216M10.7427 10.861L13.0078 13.1261M4.99476 5.11305L7.25997 7.37826M7.14874 10.7525L4.88369 13.0176M12.8967 5.00452L10.6315 7.26974M6.08663 9.00977C6.08663 7.40076 7.39099 6.0964 9 6.0964C10.609 6.0964 11.9134 7.40076 11.9134 9.00977C11.9134 10.6188 10.609 11.9231 9 11.9231C7.39099 11.9231 6.08663 10.6188 6.08663 9.00977ZM3.10575 3.11551C3.58524 2.63602 4.36265 2.63602 4.84214 3.11551C5.32164 3.59501 5.32164 4.37242 4.84214 4.85191C4.36265 5.3314 3.58524 5.3314 3.10575 4.85191C2.62625 4.37242 2.62625 3.59501 3.10575 3.11551ZM13.1579 13.1676C13.6373 12.6881 14.4148 12.6881 14.8943 13.1676C15.3737 13.6471 15.3737 14.4245 14.8943 14.904C14.4148 15.3835 13.6373 15.3835 13.1579 14.904C12.6784 14.4245 12.6784 13.6471 13.1579 13.1676ZM14.8948 3.11549C15.3743 3.59498 15.3743 4.37239 14.8948 4.85189C14.4153 5.33138 13.6379 5.33138 13.1584 4.85189C12.6789 4.37239 12.6789 3.59498 13.1584 3.11549C13.6379 2.636 14.4153 2.636 14.8948 3.11549ZM4.84214 13.1676C5.32164 13.6471 5.32164 14.4245 4.84214 14.904C4.36265 15.3835 3.58524 15.3835 3.10575 14.904C2.62625 14.4245 2.62625 13.6471 3.10575 13.1676C3.58524 12.6881 4.36265 12.6881 4.84214 13.1676Z"
stroke="#A0A0A0"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
@@ -404,17 +404,17 @@ export function IconMenuKotlin({ width = 16, height = 16 }: HomeMenuIcon) {
export function IconMenuStatus({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
width={width}
height={height}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke="currentColor"
strokeLinejoin="bevel"
d="M8.9895 1.13281C4.64498 1.13281 1.12305 4.65474 1.12305 8.99927C1.12305 13.3438 4.64498 16.8657 8.9895 16.8657C13.334 16.8657 16.856 13.3438 16.856 8.99927C16.856 7.13428 16.207 5.42087 15.1225 4.07258L8.35267 10.8424L6.39571 8.88546M9.06632 4.19659C6.42145 4.19659 4.27737 6.34068 4.27737 8.98555C4.27737 11.6304 6.42145 13.7745 9.06632 13.7745C11.7112 13.7745 13.8553 11.6304 13.8553 8.98555C13.8553 7.99916 13.5571 7.08243 13.0459 6.32059"
stroke="#A0A0A0"
strokeMiterlimit="10"
d="M8.99 1.133a7.866 7.866 0 1 0 6.133 2.94l-6.77 6.77-1.956-1.958m2.67-4.688a4.789 4.789 0 1 0 3.98 2.124"
strokeLinejoin="bevel"
/>
</svg>
)
@@ -1,4 +1,4 @@
import { References, HomepageMenuItems } from '../Navigation.types'
import type { HomepageMenuItems, NavMenuConstant, References } from '../Navigation.types'
export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
[
@@ -194,7 +194,7 @@ export const REFERENCES: References = {
},
}
export const gettingstarted = {
export const gettingstarted: NavMenuConstant = {
icon: 'getting-started',
title: 'Getting Started',
items: [
@@ -321,7 +321,7 @@ export const SocialLoginItems = [
url: '/guides/auth/social-login/auth-apple',
},
{
name: 'Azure',
name: 'Azure (Microsoft)',
icon: '/docs/img/icons/microsoft-icon',
url: '/guides/auth/social-login/auth-azure',
},
@@ -507,7 +507,7 @@ export const auth = {
],
}
export const database = {
export const database: NavMenuConstant = {
icon: 'database',
title: 'Database',
url: '/guides/database',
@@ -518,6 +518,8 @@ export const database = {
{ name: 'Database Webhooks', url: '/guides/database/webhooks' },
{ name: 'Full Text Search', url: '/guides/database/full-text-search' },
{ name: 'Database Testing', url: '/guides/database/testing' },
{ name: 'Managing Secrets with Vault', url: '/guides/database/vault' },
{ name: 'Column Encryption', url: '/guides/database/column-encryption' },
{
name: 'Extensions',
url: undefined,
@@ -662,7 +664,7 @@ export const database = {
],
}
export const api = {
export const api: NavMenuConstant = {
icon: 'serverless-apis',
title: 'Serverless APIs',
url: '/guides/api',
@@ -700,7 +702,7 @@ export const api = {
],
}
export const functions = {
export const functions: NavMenuConstant = {
icon: 'edge-functions',
title: 'Edge Functions',
url: '/guides/functions',
@@ -778,7 +780,7 @@ export const functions = {
],
}
export const realtime = {
export const realtime: NavMenuConstant = {
icon: 'realtime',
title: 'Realtime',
url: '/guides/realtime',
@@ -841,7 +843,7 @@ export const realtime = {
],
}
export const storage = {
export const storage: NavMenuConstant = {
icon: 'storage',
title: 'Storage',
url: '/guides/storage',
@@ -855,7 +857,7 @@ export const storage = {
],
}
export const supabase_cli = {
export const supabase_cli: NavMenuConstant = {
icon: 'reference-cli',
title: 'Supabase CLI',
url: '/guides/cli',
@@ -877,7 +879,7 @@ export const supabase_cli = {
],
}
export const platform = {
export const platform: NavMenuConstant = {
icon: 'platform',
title: 'Platform',
url: '/guides/platform',
@@ -932,9 +934,19 @@ export const platform = {
],
},
{
name: 'Go-live Checklist',
name: 'Production Readiness',
url: undefined,
items: [{ name: 'Production Readiness', url: '/guides/platform/going-into-prod' }],
items: [
{
name: 'Shared Responsibility Model',
url: '/guides/platform/shared-responsibility-model',
},
{
name: 'Maturity Model',
url: '/guides/platform/maturity-model',
},
{ name: 'Production Checklist', url: '/guides/platform/going-into-prod' },
],
},
{
name: 'Troubleshooting',
@@ -953,7 +965,7 @@ export const platform = {
],
}
export const resources = {
export const resources: NavMenuConstant = {
icon: 'resources',
title: 'Resources',
url: '/guides/resources',
@@ -994,7 +1006,7 @@ export const resources = {
],
}
export const self_hosting = {
export const self_hosting: NavMenuConstant = {
title: 'Self-Hosting',
icon: 'self-hosting',
url: '/guides/self-hosting',
@@ -1051,7 +1063,7 @@ export const migrate = {
],
}
export const integrations = {
export const integrations: NavMenuConstant = {
icon: 'integrations',
title: 'Integrations',
url: '/guides/integrations',
@@ -1082,6 +1094,7 @@ export const integrations = {
name: 'Developer Tools',
url: undefined,
items: [
{ name: 'Cloudflare Workers', url: '/guides/integrations/cloudflare-workers' },
{ name: 'Estuary', url: '/guides/integrations/estuary' },
{ name: 'OpenAI', url: '/guides/functions/examples/openai' },
{ name: 'pgMustard', url: '/guides/integrations/pgmustard' },
@@ -48,7 +48,7 @@ const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => {
}
const Divider = () => {
return <div className="h-px w-full bg-green-500 my-3"></div>
return <div className="h-px w-full bg-scale-500 my-3"></div>
}
const MenuSections = [
@@ -46,7 +46,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
if (activeItem && activeItemRef.current) {
// this is a hack, but seems a common one on Stackoverflow
setTimeout(() => {
activeItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
activeItemRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}, 0)
}
})
@@ -54,7 +54,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
<>
{props.subItemIndex === 0 && (
<>
<div className="h-px w-full bg-green-500 my-3"></div>
<div className="h-px w-full bg-scale-500 my-3"></div>
<span className="font-mono text-xs uppercase text-scale-1200 font-medium tracking-wider">
{props.parent.name}
</span>
@@ -147,7 +147,7 @@ const Content = (props) => {
const { menu, id } = props
return (
<ul className={['relative w-full flex flex-col gap-0'].join(' ')}>
<ul className={['relative w-full flex flex-col gap-0 pb-5'].join(' ')}>
<Link href={`${menu.parent ?? '/'}`} passHref>
<a
className={[
@@ -0,0 +1,40 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useMenuActiveRefId } from '~/hooks/useMenuState'
import Admonition from '../Admonition'
import { ICommonSection } from './Reference.types'
export interface OldVersionAlertProps {
sections: ICommonSection[]
}
const OldVersionAlert = ({ sections }: OldVersionAlertProps) => {
const router = useRouter()
const activeRefId = useMenuActiveRefId()
const activeSection = sections.find(({ id }) => id === activeRefId)
// Remove the version number from URL to get the latest
const latestVersionUrl = router.asPath
.split('/')
.slice(0, -2)
.concat(activeSection ? [activeSection.slug] : [])
.join('/')
return (
<div className="sticky top-10 z-10 lg:top-14 lg:w-1/2">
<Admonition type="caution">
You&apos;re viewing an older version of this library.
<br />
<Link href={latestVersionUrl}>
<a className="underline decoration-brand-700 underline-offset-4 decoration-1">
Switch to the latest
</a>
</Link>
.
</Admonition>
</div>
)
}
export default OldVersionAlert
@@ -8,6 +8,7 @@ import RefFunctionSection from '~/components/reference/RefFunctionSection'
import RefSubLayout from '~/layouts/ref/RefSubLayout'
import ApiOperationSection from './ApiOperationSection'
import CliCommandSection from './CLICommandSection'
import OldVersionAlert from './OldVersionAlert'
import { IAPISpec, ICommonSection, IRefStaticDoc, ISpec, TypeSpec } from './Reference.types'
interface RefSectionHandlerProps {
@@ -16,12 +17,13 @@ interface RefSectionHandlerProps {
typeSpec?: TypeSpec
pageProps: { docs: IRefStaticDoc[] }
type: 'client-lib' | 'cli' | 'api'
isOldVersion?: boolean
}
const RefSectionHandler = (props: RefSectionHandlerProps) => {
const router = useRouter()
const slug = router.query.slug[0]
const [slug] = router.query.slug
// When user lands on a url like http://supabase.com/docs/reference/javascript/sign-up
// find the #sign-up element and scroll to that
@@ -30,7 +32,7 @@ const RefSectionHandler = (props: RefSectionHandlerProps) => {
}, [slug])
useEffect(() => {
function handler(e: PopStateEvent) {
function handler() {
const [slug] = window.location.pathname.split('/').slice(-1)
document.getElementById(slug)?.scrollIntoView()
}
@@ -69,6 +71,7 @@ const RefSectionHandler = (props: RefSectionHandlerProps) => {
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
{props.isOldVersion && <OldVersionAlert sections={props.sections} />}
<RefSubLayout>
{props.sections.map((section, i) => {
const sectionType = section.type
+1 -1
View File
@@ -10,7 +10,7 @@ const authProviders = [
authType: 'social',
},
{
name: 'Azure',
name: 'Azure (Microsoft)',
logo: '/docs/img/icons/microsoft-icon',
href: '/guides/auth/social-login/auth-azure',
official: false,
-10
View File
@@ -1,10 +0,0 @@
[
{ "title": "Contributing", "url": "/handbook/contributing" },
{ "title": "Changelog", "url": "https://supabase.com/changelog" },
{
"title": "Author Styleguide",
"url": "https://github.com/supabase/supabase/blob/master/DEVELOPERS.md"
},
{ "title": "Open Source", "url": "https://supabase.com/open-source" },
{ "title": "SupaSquad", "url": "https://supabase.com/supasquad" }
]
+32
View File
@@ -0,0 +1,32 @@
import { IconCheckCircle, IconLifeBuoy } from '~/../../packages/ui'
export const primaryLinks = [
{
featherIcon: IconLifeBuoy,
text: 'Need some help?',
ctaLabel: 'Contact support',
url: 'https://app.supabase.com/support/new',
},
{
icon: 'M12.9521 10.5073C12.766 10.3212 12.5289 10.1943 12.2708 10.1427L10.6794 9.82467C9.80719 9.65024 8.90169 9.77152 8.10609 10.1693L7.89409 10.2747C7.0985 10.6725 6.193 10.7938 5.32076 10.6193L4.03343 10.362C3.81823 10.319 3.59574 10.3298 3.38571 10.3934C3.17569 10.457 2.9846 10.5715 2.82943 10.7267M5.33343 2.88867H10.6668L10.0001 3.55534V7.00334C10.0002 7.35693 10.1407 7.69602 10.3908 7.94601L13.7241 11.2793C14.5641 12.1193 13.9688 13.5553 12.7808 13.5553H3.21876C2.03076 13.5553 1.43609 12.1193 2.27609 11.2793L5.60943 7.94601C5.85949 7.69602 6.00002 7.35693 6.00009 7.00334V3.55534L5.33343 2.88867Z',
text: 'Lastest product updates?',
ctaLabel: 'See Changelog',
url: 'https://supabase.com/changelog',
},
{
featherIcon: IconCheckCircle,
text: "Something's not right?",
ctaLabel: 'Check system status',
url: 'https://status.supabase.com/',
},
]
export const secondaryLinks = [
{ title: 'Contributing', url: '/handbook/contributing' },
{
title: 'Author Styleguide',
url: 'https://github.com/supabase/supabase/blob/master/DEVELOPERS.md',
},
{ title: 'Open Source', url: 'https://supabase.com/open-source' },
{ title: 'SupaSquad', url: 'https://supabase.com/supasquad' },
]
+1
View File
@@ -1,5 +1,6 @@
---
title: Global Flags
subtitle: 'Supabase CLI supports global flags for every command.'
description: 'Supabase CLI supports global flags for every command.'
---
+9
View File
@@ -0,0 +1,9 @@
import type { Config } from '@jest/types'
const config: Config.InitialOptions = {
preset: 'ts-jest',
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
testEnvironment: 'jsdom',
}
export default config
+22 -18
View File
@@ -21,7 +21,7 @@ interface Props {
}
const Layout: FC<Props> = (props: Props) => {
const { asPath, basePath } = useRouter()
const { asPath } = useRouter()
const hasTableOfContents =
props.toc !== undefined &&
@@ -60,26 +60,30 @@ const Layout: FC<Props> = (props: Props) => {
},
}}
/>
<div className={['relative transition-all ease-out', 'duration-150 '].join(' ')}>
{/* <p className="text-brand-900 tracking-wider">Tutorials</p> */}
<article className="prose dark:prose-dar max-w-none">
<h1>{props.meta.title}</h1>
{/* <div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-800 to-brand-900 my-16"></div> */}
<MDXProvider components={components} children={props.children} />
</article>
</div>
{hasTableOfContents && !props.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 md:block md:col-span-3 px-2',
].join(' ')}
>
<TableOfContents toc={props.toc} video={props.meta?.video} />
<LayoutMainContent>
<div className={['relative transition-all ease-out', 'duration-150 '].join(' ')}>
<article className="prose dark:prose-dar max-w-none">
<h1>{props.meta.title}</h1>
<MDXProvider components={components} children={props.children} />
</article>
</div>
)}
{hasTableOfContents && !props.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 md:block md:col-span-3 px-2',
].join(' ')}
>
<TableOfContents toc={props.toc} video={props.meta.video} />
</div>
)}
</LayoutMainContent>
</>
)
}
export const LayoutMainContent: FC<{ className?: string }> = ({ className, children }) => (
<div className={['max-w-7xl px-5 mx-auto py-16', className].join(' ')}>{children}</div>
)
export default Layout
+86
View File
@@ -0,0 +1,86 @@
import { MDXProvider } from '@mdx-js/react'
import { NextSeo } from 'next-seo'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { FC } from 'react'
import components from '~/components'
import TableOfContents from '~/components/TableOfContents'
import HomePageCover from '../components/HomePageCover'
import { LayoutMainContent } from './DefaultLayout'
interface Props {
meta: {
title: string
description?: string
hide_table_of_contents?: boolean
video?: string
canonical?: string
}
children: any
toc?: any
menuItems: any
}
const HomeLayout: FC<Props> = (props: Props) => {
const { asPath } = useRouter()
const hasTableOfContents =
props.toc !== undefined &&
props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0
return (
<>
<Head>
<title>{asPath === '/' ? 'Supabase Docs' : `${props.meta?.title} | Supabase Docs`}</title>
<meta name="description" content={props.meta?.description} />
<meta property="og:image" content={`https://supabase.com/docs/img/supabase-og-image.png`} />
<meta
name="twitter:image"
content={`https://supabase.com/docs/img/supabase-og-image.png`}
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<NextSeo
canonical={props.meta?.canonical ?? `https://supabase.com/docs${asPath}`}
openGraph={{
url: `https://supabase.com/docs${asPath}`,
type: 'article',
videos: props.meta?.video && [
{
// youtube based video meta
url: props.meta?.video,
width: 640,
height: 385,
type: 'application/x-shockwave-flash',
},
],
article: {
publishedTime: new Date().toISOString(),
modifiedTime: new Date().toISOString(),
authors: ['Supabase'],
},
}}
/>
<HomePageCover meta={props.meta} />
<LayoutMainContent>
<div className={['relative transition-all ease-out', 'duration-150 '].join(' ')}>
<article className="prose dark:prose-dar max-w-none">
<MDXProvider components={components} children={props.children} />
</article>
</div>
{hasTableOfContents && !props.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 md:block md:col-span-3 px-2',
].join(' ')}
>
<TableOfContents toc={props.toc} video={props.meta.video} />
</div>
)}
</LayoutMainContent>
</>
)
}
export default HomeLayout
+1 -1
View File
@@ -339,7 +339,7 @@ const SiteLayout = ({ children }: PropsWithChildren<{}>) => {
<MobileHeader />
</div>
</div>
<div className="grow px-5 max-w-7xl mx-auto py-16">
<div className="grow">
{children}
<Footer />
</div>
+63 -63
View File
@@ -5,20 +5,21 @@ import Link from 'next/link'
import { useRouter } from 'next/router'
import { FC, useEffect, useRef, useState } from 'react'
import { IconExternalLink } from 'ui'
import ExpandableVideo from 'ui/src/components/ExpandableVideo/ExpandableVideo'
import components from '~/components'
import { highlightSelectedTocItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils'
import FooterHelpCallout, { FooterHelpCalloutType } from '~/components/FooterHelpCallout'
import { FooterHelpCalloutType } from '~/components/FooterHelpCallout'
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
import useHash from '~/hooks/useHash'
import { LayoutMainContent } from '../DefaultLayout'
import ExpandableVideo from 'ui/src/components/ExpandableVideo/ExpandableVideo'
interface Props {
meta: {
title: string
description?: string
description?: string // used in meta tags
hide_table_of_contents?: boolean
breadcrumb?: string
subtitle?: string
subtitle?: string // used on the page under the H1
footerHelpType?: FooterHelpCalloutType
video?: string
tocVideo?: string
@@ -105,74 +106,73 @@ const Layout: FC<Props> = (props) => {
},
}}
/>
<div className={['grid grid-cols-12 relative gap-4'].join(' ')}>
<div
className={[
'relative',
!props.hideToc ? 'col-span-12 md:col-span-9' : 'col-span-12',
'transition-all ease-out',
'duration-100',
].join(' ')}
>
{props.meta.breadcrumb && (
<p className="text-brand-900 tracking-wider mb-3">{props.meta.breadcrumb}</p>
)}
<article
ref={articleRef}
className={`${
props.meta?.hide_table_of_contents || !hasTableOfContents ? '' : ''
} prose dark:prose-dark max-w-none`}
>
<h1 className="mb-0">{props.meta.title}</h1>
{props.meta.subtitle && (
<h2 className="mt-3 text-xl text-scale-1100">{props.meta.subtitle}</h2>
)}
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-800 to-brand-900 my-8"></div>
<MDXProvider components={components}>{props.children}</MDXProvider>
{EDIT_BUTTON_EXCLUDE_LIST.includes(router.route) ? (
<></>
) : (
<div className="mt-16 not-prose">
<div>
<Link
href={`https://github.com/supabase/supabase/edit/master/apps/docs/pages${router.asPath}.mdx`}
passHref
>
<a className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200">
Edit this page on GitHub <IconExternalLink size={14} strokeWidth={1.5} />
</a>
</Link>
</div>
</div>
)}
</article>
</div>
{!props.hideToc && hasTableOfContents && !props.meta?.hide_table_of_contents && (
<LayoutMainContent className="pb-0">
<div className={['grid grid-cols-12 relative gap-4'].join(' ')}>
<div
className={[
'col-span-3',
'border-scale-400 dark:bg-scale-200 table-of-contents-height',
'thin-scrollbar overflow-y-auto sticky hidden md:block md:col-span-3 px-2',
'relative',
!props.hideToc ? 'col-span-12 md:col-span-9' : 'col-span-12',
'transition-all ease-out',
'duration-100',
].join(' ')}
>
<div className="border-l">
{props.meta?.tocVideo && !!tocVideoPreview && (
<div className="relative mb-6 pl-5">
<ExpandableVideo imgUrl={tocVideoPreview} videoId={props.meta.tocVideo} />
{props.meta.breadcrumb && (
<p className="text-brand-900 tracking-wider mb-3">{props.meta.breadcrumb}</p>
)}
<article
ref={articleRef}
className={`${
props.meta?.hide_table_of_contents || !hasTableOfContents ? '' : ''
} prose dark:prose-dark max-w-none`}
>
<h1 className="mb-0">{props.meta.title}</h1>
{props.meta.subtitle && (
<h2 className="mt-3 text-xl text-scale-1100">{props.meta.subtitle}</h2>
)}
<div className="w-full h-[1px] bg-scale-500 my-8"></div>
<MDXProvider components={components}>{props.children}</MDXProvider>
{EDIT_BUTTON_EXCLUDE_LIST.includes(router.route) ? (
<></>
) : (
<div className="mt-16 not-prose">
<div>
<a
href={`https://github.com/supabase/supabase/edit/master/apps/docs/pages${router.asPath}.mdx`}
className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200"
>
Edit this page on GitHub <IconExternalLink size={14} strokeWidth={1.5} />
</a>
</div>
</div>
)}
<span className="block font-mono text-xs uppercase text-scale-1200 px-5 mb-6">
On this page
</span>
<GuidesTableOfContents list={tocList} />
</div>
</article>
</div>
)}
</div>
<FooterHelpCallout footerHelpType={props.meta?.footerHelpType} title={props.meta?.title} />
{!props.hideToc && hasTableOfContents && !props.meta?.hide_table_of_contents && (
<div
className={[
'col-span-3',
'border-scale-400 dark:bg-scale-200 table-of-contents-height',
'thin-scrollbar overflow-y-auto sticky hidden md:block md:col-span-3 px-2',
'transition-all ease-out',
'duration-100',
].join(' ')}
>
<div className="border-l">
{props.meta?.tocVideo && !!tocVideoPreview && (
<div className="relative mb-6 pl-5">
<ExpandableVideo imgUrl={tocVideoPreview} videoId={props.meta.tocVideo} />
</div>
)}
<span className="block font-mono text-xs uppercase text-scale-1200 px-5 mb-6">
On this page
</span>
<GuidesTableOfContents list={tocList} />
</div>
</div>
)}
</div>
</LayoutMainContent>
</>
)
}
+6 -2
View File
@@ -51,7 +51,11 @@ interface ISectionDetails {}
interface ISectionExamples {}
const RefSubLayout: FC<RefSubLayoutType> & RefSubLayoutSubComponents = (props) => {
return <div className="flex flex-col w-full divide-y">{props.children}</div>
return (
<div className="flex flex-col w-full divide-y px-5 max-w-7xl mx-auto py-16">
{props.children}
</div>
)
}
const Section: FC<ISectionContainer> = (props) => {
@@ -128,7 +132,7 @@ const StickyHeader: FC<StickyHeader> = ({ icon, ...props }) => {
}
const Details: FC<ISectionDetails> = (props) => {
return <div className="relative overflow-hidden w-full">{props.children}</div>
return <div className="relative w-full">{props.children}</div>
}
const Examples: FC<ISectionExamples> = (props) => {
+4 -1
View File
@@ -1,3 +1,4 @@
// @ts-check
import nextMdx from '@next/mdx'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
@@ -61,10 +62,12 @@ const nextConfig = {
'raw.githubusercontent.com',
'weweb-changelog.ghost.io',
'img.youtube.com',
'archbee-image-uploads.s3.amazonaws.com'
],
},
experimental: {
mdxRs: true,
// TODO: @next/mdx ^13.0.2 only supports experimental mdxRs flag. next ^13.0.2 will stop warning about this being unsupported.
// mdxRs: true,
modularizeImports: {
lodash: {
transform: 'lodash/{{member}}',
+2
View File
@@ -9,6 +9,7 @@
"build:analyze": "ANALYZE=true next build",
"start": "next start",
"lint": "next lint",
"test": "jest",
"build:sitemap": "node ./internals/generate-sitemap.mjs",
"embeddings": "tsx scripts/search/generate-embeddings.ts",
"embeddings:refresh": "npm run embeddings -- --refresh",
@@ -61,6 +62,7 @@
"clsx": "^1.2.1",
"common": "*",
"config": "*",
"framer-motion": "^6.5.1",
"github-slugger": "^2.0.0",
"gray-matter": "^4.0.3",
"isbot": "^3.6.5",
+36 -14
View File
@@ -2,7 +2,7 @@ import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
import { AuthProvider, ThemeProvider, useTelemetryProps } from 'common'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { AppPropsWithLayout } from 'types'
import { CommandMenuProvider } from 'ui'
import Favicons from '~/components/Favicons'
@@ -22,17 +22,20 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
IS_PLATFORM || LOCAL_SUPABASE ? createBrowserSupabaseClient() : undefined
)
function handlePageTelemetry(route: string) {
return post(`${API_URL}/telemetry/page`, {
referrer: document.referrer,
title: document.title,
route,
ga: {
screen_resolution: telemetryProps?.screenResolution,
language: telemetryProps?.language,
},
})
}
const handlePageTelemetry = useCallback(
(route: string) => {
return post(`${API_URL}/telemetry/page`, {
referrer: document.referrer,
title: document.title,
route,
ga: {
screen_resolution: telemetryProps?.screenResolution,
language: telemetryProps?.language,
},
})
},
[telemetryProps]
)
useEffect(() => {
function handleRouteChange(url: string) {
@@ -59,7 +62,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [router.events])
}, [router, handlePageTelemetry])
useEffect(() => {
/**
@@ -68,7 +71,26 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
if (router.isReady) {
handlePageTelemetry(router.basePath + router.asPath)
}
}, [router.isReady])
}, [router, handlePageTelemetry])
/**
* Reference docs use `history.pushState()` to jump to
* sub-sections without causing a re-render.
*
* We need to the below handler to manually force a re-render
* when navigating away from, then back to reference docs
*/
useEffect(() => {
function handler() {
router.replace(window.location.href)
}
window.addEventListener('popstate', handler)
return () => {
window.removeEventListener('popstate', handler)
}
}, [router])
const SITE_TITLE = 'Supabase Documentation'
+1 -3
View File
@@ -3,9 +3,7 @@ import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
</Head>
<Head />
<body className="dark">
<Main />
<NextScript />
@@ -28,12 +28,10 @@ This creates a corresponding route `todos` which can accept `GET`, `POST`, `PATC
1. Click **New Column** and create a column with the name `task` and type `text`.
1. Click **Save**.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -60,12 +58,10 @@ Every Supabase project has a unique API URL. Your API is secured behind an API g
2. Click **API** in the sidebar.
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -207,12 +203,10 @@ By default Realtime is disabled on your database. Let's turn on Realtime for the
3. Control which database events are sent by toggling **Insert**, **Update**, and **Delete**.
4. Control which tables broadcast changes by selecting **Source** and toggling each table.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-realtime.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
+1 -3
View File
@@ -44,12 +44,10 @@ We'll create a database table called `todos` for storing tasks. This creates a c
</TabPanel>
<TabPanel id="dashboard" label="Dashboard">
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -12,12 +12,10 @@ Supabase generates documentation in the [Dashboard](https://app.supabase.com) wh
2. Select any table under **Tables and Views** in the sidebar.
3. Switch between the JavaScript and the cURL docs using the tabs.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-docs.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -37,7 +37,7 @@ npx supabase login
Generate types for your project to produce the `types/supabase.ts` file:
```bash
npx supabase gen types typescript --project-id "$PROJECT_ID" --schema public > types/supabase.ts
npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts
```
After you have generated your types, you can use them in `src/index.ts`
@@ -65,7 +65,7 @@ One way to keep your type definitions in sync with your database is to set up a
Add the script above to your `package.json` to run it using `npm run update-types`
```json
"update-types": "npx supabase gen types typescript --project-id \"$PROJECT_ID\" > types/supabase.ts"
"update-types": "npx supabase gen types typescript --project-id \"$PROJECT_REF\" > types/supabase.ts"
```
Create a file `.github/workflows/update-types.yml` with the following snippet to define the action along with the environment variables. This script will commit new type changes to your repo every night.
@@ -83,7 +83,7 @@ jobs:
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
PROJECT_ID: <your-project-id>
PROJECT_REF: <your-project-id>
steps:
- uses: actions/checkout@v2
with:
+3 -9
View File
@@ -123,12 +123,10 @@ Get started with our [Row Level Security Guides](/docs/guides/auth/row-level-sec
Authentication only gets you so far. When you need granular authorization rules, nothing beats PostgreSQL's [Row Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html). Supabase makes it simple to turn RLS on and off.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/rls-zoom2.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -136,12 +134,10 @@ Authentication only gets you so far. When you need granular authorization rules,
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are PostgreSQL's rule engine. They are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/policies-zoom2.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -189,12 +185,10 @@ Supabase provides multiple endpoints to authenticate and manage your users:
When users sign up, Supabase assigns them a unique ID. You can reference this ID anywhere in your database. For example, you might create a `profiles` table referencing `id` in the `auth.users` table using a `user_id` field.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/auth-zoom2.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -9,13 +9,8 @@ export const meta = {
Auth UI is a pre-built React component for authenticating users.
It supports custom themes and extensible styles to match your brand and aesthetic.
<video width="99%" muted playsInline controls="true">
<source
src="https://supabase.com/images/blog/lw5-one-more/auth-ui-demo.mp4"
type="video/mp4"
muted
playsInline
/>
<video width="99%" muted playsInline controls={true}>
<source src="https://supabase.com/images/blog/lw5-one-more/auth-ui-demo.mp4" type="video/mp4" />
</video>
## Set up Auth UI
@@ -10,7 +10,7 @@ export const meta = {
The Next.js Auth Helpers package configures Supabase Auth to store the user's session in a cookie, rather than `localStorage`. This makes the users's session available server-side - in Server Components and Route Handlers - and is automatically sent along with any requests to Supabase.
> The `app` directory in Next.js is still in beta and expected to change. For examples using the `pages` directory check out [Auth Helpers in Next.js](/docs/guides/auth/auth-helpers/nextjs).
> Note: If you are using the `pages` directory, check out [Auth Helpers in Next.js](/docs/guides/auth/auth-helpers/nextjs).
<div className="video-container">
<iframe
@@ -38,8 +38,6 @@ The Next.js Auth Helpers package configures Supabase Auth to store the user's se
npm install @supabase/auth-helpers-nextjs
```
> [Next.js Server Components](https://beta.nextjs.org/docs) and the `app` directory are experimental and likely to change.
</TabPanel>
<TabPanel id="yarn" label="Yarn">
@@ -47,8 +45,6 @@ npm install @supabase/auth-helpers-nextjs
yarn add @supabase/auth-helpers-nextjs
```
> [Next.js Server Components](https://beta.nextjs.org/docs) and the `app` directory are experimental and likely to change.
</TabPanel>
</Tabs>
@@ -182,7 +178,7 @@ Create a new file at `/app/supabase-provider.tsx` and populate with the followin
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { Session, createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { useRouter } from 'next/navigation'
import type { SupabaseClient } from '@supabase/auth-helpers-nextjs'
@@ -253,6 +249,8 @@ export const useSupabase = () => {
Modify `layout.jsx` to wrap the application with the `<SupabaseProvider>` component:
```jsx title="app/layout.jsx"
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { headers, cookies } from 'next/headers'
import './globals.css'
import SupabaseProvider from './supabase-provider'
@@ -261,14 +259,20 @@ export const metadata = {
description: 'Generated by create next app',
}
export default function RootLayout({ children }) {
export default async function RootLayout({ children }) {
const supabase = createServerComponentSupabaseClient({
headers,
cookies,
})
const {
data: { session },
} = await supabase.auth.getSession()
return (
<html lang="en">
<body>
<SupabaseProvider session={session}>
<SupabaseListener serverAccessToken={session?.access_token} />
{children}
</SupabaseProvider>
<SupabaseProvider session={session}>{children}</SupabaseProvider>
</body>
</html>
)
@@ -282,6 +286,8 @@ export default function RootLayout({ children }) {
Modify `layout.tsx` to wrap the application with the `<SupabaseProvider>` component:
```tsx title="app/layout.tsx"
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { headers, cookies } from 'next/headers'
import './globals.css'
import SupabaseProvider from './supabase-provider'
@@ -290,14 +296,20 @@ export const metadata = {
description: 'Generated by create next app',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const supabase = createServerComponentSupabaseClient({
headers,
cookies,
})
const {
data: { session },
} = await supabase.auth.getSession()
return (
<html lang="en">
<body>
<SupabaseProvider session={session}>
<SupabaseListener serverAccessToken={session?.access_token} />
{children}
</SupabaseProvider>
<SupabaseProvider session={session}>{children}</SupabaseProvider>
</body>
</html>
)
@@ -28,7 +28,7 @@ This library supports the following tooling versions:
- Node.js: `^10.13.0 || >=12.0.0`
- Next.js: `>=10`
> Note: Next.js 13 is stable, however, the new `app` directory and Server Components are still in beta. Check out our experimental guide on [using Auth Helpers with Next.js Server Components](/docs/guides/auth/auth-helpers/nextjs-server-components).
> Note: As of [Next.js 13.4](https://nextjs.org/blog/next-13-4), the `app` directory and Server Components have reached stable status. Check out our guide on using [Auth Helpers with Next.js Server Components](/docs/guides/auth/auth-helpers/nextjs-server-components).
Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks.
@@ -438,11 +438,6 @@ export const handle: Handle = async ({ event, resolve }) => {
}
return resolve(event, {
/**
* There´s an issue with `filterSerializedResponseHeaders` not working when using `sequence`
*
* https://github.com/sveltejs/kit/issues/8061
*/
filterSerializedResponseHeaders(name) {
return name === 'content-range'
},
@@ -286,43 +286,24 @@ using (
## Tips
### Enable Realtime for database tables
Realtime server broadcasts database changes to authorized users depending on your Row Level Security (RLS) policies.
We recommend that you enable row level security and set row security policies on tables that you add to the publication.
However, you may choose to disable RLS on a table and have changes broadcast to all connected clients.
```sql
/**
* REALTIME SUBSCRIPTIONS
* Realtime enables listening to any table in your public schema.
*/
begin;
-- remove the realtime publication
drop publication if exists supabase_realtime;
-- re-create the publication but don't enable it for any tables
create publication supabase_realtime;
commit;
-- add a table to the publication
alter publication supabase_realtime add table products;
-- add other tables to the publication
alter publication supabase_realtime add table posts;
```
### You don't have to use policies
You can also put your authorization rules in your middleware, similar to how you would create security rules with any other `backend <-> middleware <-> frontend` architecture.
You can also put your authorization rules in your middleware, similar to how you would create security rules with any other `backend <-> middleware <-> frontend` architecture. You can use Edge Functions to run this architecture, or you can use your favorite server framework, like Rails, Django, Node.js, Phoenix, or Laravel.
Policies are a tool. In the case of "serverless/Jamstack" setups, they are especially effective because you don't have to deploy any middleware at all.
Policies are a tool. In the case of "serverless/Jamstack" setups, they are especially effective because you don't have to deploy any middleware at all and can use the javascript libraries directly from the browser.
However, if you want to use another authorization method for your applications, that's also fine. Supabase is "just Postgres", so if your application
works with Postgres, then it also works with Supabase.
That said, if you want to use another authorization method for your applications that's also fine. Supabase is "just Postgres", so if your application works with Postgres, then it also works with Supabase.
Tip: Make sure to enable RLS for all your tables, so that your tables are inaccessible. Then use the "Service" which we provide, which is designed to bypass RLS.
If you plan to use this approach make sure to enable RLS for your tables. Then use the `service_role` key (for our client libraries) or the `postgres` role - both of these can bypass RLS. You don't need to create any policies with this approach, simply enabling RLS is sufficient:
```sql
create table profiles (
id serial primary key,
email text
);
alter table profiles enable row level security;
```
### Never use a service key on the client
@@ -306,3 +306,7 @@ export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
### Which authentication flows have PKCE support?
At present, PKCE is supported on the Magic Link, OAuth, Sign Up, and Password Recovery routes. These correspond to the `signInWithOtp`, `signInWithOAuth`, `signUp`, and `resetPasswordForEmail` methods on the Supabase client library. When using PKCE with Phone and Email OTPs, there is no behavior change with respect to the implicit flow - an access token will be returned in the body when a request is successful.
@@ -2,11 +2,11 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-azure',
title: 'Login with Azure',
description: 'Add Azure OAuth to your Supabase project',
title: 'Login with Azure (Microsoft)',
description: 'Add Azure (Microsoft) OAuth to your Supabase project',
}
To enable Azure Auth for your project, you need to set up an Azure OAuth application and add the application credentials to your Supabase Dashboard.
To enable Azure (Microsoft) Auth for your project, you need to set up an Azure OAuth application and add the application credentials to your Supabase Dashboard.
## Overview
@@ -91,6 +91,21 @@ async function signout() {
}
```
## Obtain the provider refresh token
Azure OAuth2.0 doesn't return the `provider_refresh_token` by default. If you need the `provider_refresh_token` returned, you will need to include the following scope:
```js
async function signInWithAzure() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'azure',
options: {
scopes: 'offline_access',
},
})
}
```
## Resources
- [Azure Developer Account](https://portal.azure.com)
@@ -83,6 +83,18 @@ async function signInWithGoogle() {
}
```
You can view the full list of query parameters and their descriptions [here](https://developers.google.com/identity/protocols/oauth2/web-server#creatingclient).
When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage:
```js
async function signout() {
const { error } = await supabase.auth.signOut()
}
```
## Obtain the provider refresh token
Google OAuth2.0 doesn't return the `provider_refresh_token` by default. If you need the `provider_refresh_token` returned, you will need to add additional query parameters:
```js
@@ -99,16 +111,6 @@ async function signInWithGoogle() {
}
```
You can view the full list of query parameters and their descriptions [here](https://developers.google.com/identity/protocols/oauth2/web-server#creatingclient).
When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage:
```js
async function signout() {
const { error } = await supabase.auth.signOut()
}
```
## Resources
- [Supabase Account - Free Tier OK](https://supabase.com)
@@ -30,7 +30,7 @@ Setting up Slack logins for your application consists of 3 parts:
## Create a Slack OAuth app
- Go to [api.slack.com](https://api.slack.com/apps).
- Click on `Create an App`
- Click on `Create New App`
Under `Create an app...`:
@@ -43,14 +43,17 @@ Under `App Credentials`:
- Copy and save your newly-generated `Client ID`
- Copy and save your newly-generated `Client Secret`
- Click `Permissions`
Under `Redirect URLs`:
Under the sidebar, select `OAuth & Permissions` and look for `Redirect URLs`:
- Click `Add New Redirect URL`
- Paste your `Callback URL` then click `Add`
- Click `Save URLs`
Under `Scopes`:
- Add the following scopes under the `User Token Scopes`: `profile`, `email`, `openid`. These scopes are the default scopes that Supabase Auth uses to request for user information. You can add any additional scopes that you may need as well.
## Enter your Slack credentials into your Supabase Project
<SocialProviderSettingsSupabase provider="Slack" />
@@ -41,10 +41,18 @@ Under `App credentials`
- Copy and save your `Client ID`.
- Copy and save your `Client secret`.
- Add your `Callback URL` in the OAuth allow list.
Under `Redirect URL for OAuth`
- Paste your `Callback URL`
Under `Scopes`
- Click on `Add scopes`
- Click on `User`
- Choose `user:read`
- Click `Done`
- Click `Continue`
## Enter your Zoom credentials into your Supabase Project
+1 -7
View File
@@ -33,7 +33,7 @@ You don't have to be a database expert to start using Supabase. Our table view m
Dig into the relationships within your data.
<video width="99%" loop="" muted="" playsInline="" controls="true">
<video width="99%" loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/relational-drilldown-zoom.mp4"
type="video/mp4"
@@ -48,8 +48,6 @@ You can duplicate your tables, just like you would inside a spreadsheet.
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/duplicate-tables.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -61,8 +59,6 @@ Supabase comes with a SQL Editor. You can also save your favorite queries to run
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/favorites.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -90,8 +86,6 @@ You can enable Postgres extensions with the click of a button within the Supabas
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/toggle-extensions.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -0,0 +1,249 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'column-encryption',
title: 'Column Encryption',
description: 'Use Supabase to store and serve files.',
sidebar_label: 'Overview',
video: 'https://www.youtube.com/v/J9mTPY8rIXE',
}
Encrypted Columns for Tables
## Transparent Column Encryption (TCE)
TCE provides a safe way to encrypt your data so that it doesn't leak into logs and backups. It can also provide row-level authenticated encryption.
TCE is the primary building block of [Vault](/docs/guides/database/vault), Supabase's Postgres secrets manager. Vault is a built-in table with an integrated UI intended to act as a secure global secrets management for you project. However if you need more fine-grain control over your encrypted data, such as encrypting columns in your own tables, you can use TCE directly. Any Postgres value that can be cast to `text` or `bytea` can be encrypted using TCE.
### Encrypting columns
When creating a new column in the Dashboard, you can choose to encrypt a `text` or `bytea` column. You will choose which key you would like to encrypt it with by selecting an existing key ID or creating a new one.
![Encrypting columns](/docs/img/guides/database/vault-encrypting-columns.png)
Once you've created an encrypted column, you can insert data into the table like you would any other table. For example if you were to insert an email address into an encrypted column, you will see that the address is transparently converted into an encrypted value on the new row.
![Encrypted data](/docs/img/guides/database/vault-encrypted-data.png)
Decrypted data is accessed using a special view that is automatically created after adding an encrypted column to a table. This view decrypts the data row-by-row as you access it. By default, this view is called `decrypted_<your-table-name>`. In the example below, the decryption view for the `profiles` table is called `decrypted_profiles`. Notice there is a new column in the view called `decrypted_emails` that contains the decrypted email value.
![Decrypted data](/docs/img/guides/database/vault-decrypted-data.png)
## How Key Derivation Works
The current state-of-the-art in encryption libraries is [libsodium](https://doc.libsodium.org/).
**libsodium** offers a range of APIs for authenticated secret and public key encryption, key derivation, encrypted streaming, [AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption), various forms of hashing, and much more.
This powerful API is available to PostgreSQL using the [**pgsodium**](https://github.com/michelp/pgsodium) extension. **pgsodium** provides all the functionality of the full **libsodium** API, but previously it required developers to set up database encryption themselves, which remained a challenge even for those familiar with database administration.
To solve this problem, **pgsodium** now has a full key management API, primarily via the table `pgsodium.key` and the `pgsodium.create_key()` function. This key table contains no raw keys, but instead uses libsodium Key IDs to derive keys that are used internally for encryption. A [key derivation function](https://libsodium.gitbook.io/doc/key_derivation) is used with an internal root key that is unavailable to SQL and not stored in the database, but rather managed by you externally using flexible scripts, or by Supabase automatically as part of our service offering.
The simplest way to use **pgsodium** to encrypt and decrypt data is to first create a **Key ID**. Valid Key IDs are stored in pgsodium in a special extension table, and they can be created using the `pgsodium.create_key()` function. This function takes a number of arguments depending on how it's used, but the simplest case is to create a new key with no arguments:
```sql
select * from pgsodium.create_key();
-[ RECORD 1 ]---+-------------------------------------
id | eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d
name |
status | valid
key_type | aead-det
key_id | 2
key_context | \x7067736f6469756d
created | 2022-11-13 21:19:35.765823+00
expires |
associated_data |
```
This key can now be used with pgsodium encryption functions by its UUID (`eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d`). For example:
```sql
select * from pgsodium.crypto_aead_det_encrypt (
'this is the message', -- a message to encrypt
'this is associated data', -- some authenticated associated data
'eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d'::uuid -- key ID
);
```
This produces the following encrypted “ciphertext” using the `aead-det` algorithm from the libsodium [XChaCha20-SIV](https://github.com/jedisct1/libsodium-xchacha20-siv) encryption function.
```sql
-[ RECORD 1 ]-----------+---------------------------------------------------------------------------------------------------------
crypto_aead_det_encrypt | \\x099baa820250d7375ed141f8f1936af384bc229f3de1010a6eff6ffdaf3998baffbae75b5cd83d1c469407ff2d3764a428b742
```
Now to decrypt the ciphertext, pass it to the decryption function _with the same Key ID_:
```sql
select *
from convert_from(pgsodium.crypto_aead_det_decrypt (
'\\x099baa820250d7375ed141f8f1936af384bc229f3de1010a6eff6ffdaf3998baffbae75b5cd83d1c469407ff2d3764a428b742',
'this is associated data',
'eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d'::uuid
), 'utf8');
```
Which recovers the original “plaintext” message:
```sql
-[ RECORD 1 ]+--------------------
convert_from | this is the message
```
In the above example, there is _no raw key_, only a Key ID which is used to derive the key used to encrypt the message and authenticate it with the associated data. In fact, it is impossible for a SQL user to derive the key used above, and if the Key ID is stored, then no encryption keys or decrypted information will leak into backups, disk storage, or the database WAL stream.
## Transparent Column Encryption
As of **pgsodium** 3.0.0 and up, the extension offers a simple and declarative Transparent Column Encryption feature (TCE). This feature is now shipped with all Supabase projects. TCE allows you to specify encrypted columns within a table and generates a new view that “wraps” that table to decrypt the contents.
TCE works using two dynamically generated objects for tables that contain encrypted columns:
- an `INSERT UPDATE` trigger that encrypts data when it is inserted or modified
- a view that is created to wrap the table to decrypt the data when it is accessed
To “transparently” decrypt the table, access the dynamically generated view _instead of the table_. For every encrypted column in the table, the view will have an additional decrypted column that shows the decrypted result.
It's worth noting at this point that sometimes there is some confusion about handling encrypted data with TCE. The `T` stands for **Transparent** which means, you can always see decrypted data through the view, where the decrypted data can't be see is when stored on disk, or in pg_dumps, backups, WAL streams, etc. This is often called **Encryption At Rest** and is one layer in many that may be used to encrypt and protect your data.
Often Transparent encryption is understood to be “Transparent Disk Encryption” or “Full Disk Encryption”, this is where a drive is encrypted but reading and writing that drive is decrypted. TCE is similar to this, where data on disk is encrypted, but it is more fine grained, only particular columns are encrypted. The data is also encrypted in the sense that the table stored on disk contains encrypted data, without the view or the key, pg_dumps and backups still contain encrypted data, this is not possible with disk-only encryption.
For the moment TCE only works for columns of type `text` (or types castable to `text` like `json`). Soon we will also support `bytea` and possibly more as use cases and tests get better.
TCE uses one of PostgreSQL's lesser-known features: [`SECURITY LABEL`](https://www.postgresql.org/docs/current/sql-security-label.html). A security label can be thought of as a simple label which is attached to an object (a table, column, etc). Each label is scoped to an extension and that extension can provide security features depending on the label.
Let's see a simple example of using `SECURITY LABEL` to encrypt a column using **pgsodium**.
### One Key ID for the Entire Column
For the simplest case, a column can be encrypted with one Key ID which must be of the type `aead-det` (as created above):
```sql
create table secrets (
id bigserial primary key,
secret_number text
);
security label for pgsodium
on column secrets.secret_number
is 'ENCRYPT WITH KEY ID e348034b-3f07-4878-aad6-000511d12826';
```
The advantage of this approach is simplicity - the user creates one key and labels a column with it. The cryptographic algorithm for this approach uses a _nonceless_ encryption algorithm called `crypto_aead_det_xchacha20()`. This algorithm is written by the author of libsodium and can be found [here](https://github.com/jedisct1/libsodium-xchacha20-siv).
Using one key for an entire column means that whoever can decrypt one row can decrypt them all from a database dump. Also changing (rotating) the key means rewriting the whole table.
### One Key ID per Row
A more fine grained approach would be storing one Key ID per row:
```sql
create table secrets (
id bigserial primary key,
secret text,
key_id uuid not null default 'e348034b-3f07-4878-aad6-000511d12826'::uuid
);
security label for pgsodium
on column secrets.secret
is 'ENCRYPT WITH KEY COLUMN key_id';
```
This approach ensures that a key for one user doesn't necessarily decrypt any others. While rows can share key IDs, they don't necessarily have to (unlike the first example above where one key id was used for the entire column). It also acts as a natural partition that can work in conjunction with Row Level Security to share distinct keys between owners.
Notice also how there is a `DEFAULT` value for the `key_id`. In a way, this gives you the best of both approaches - encrypting the column when a per-row key ID is not provided. The downside to this approach is that you need to store one key ID per row, which takes up more disk space (but that's cheap!).
### One Key ID per Row with Nonce Support
The default cryptographic algorithm for the above approach uses a _nonceless_ encryption algorithm called [`crypto_aead_det_xchacha20()`](https://github.com/jedisct1/libsodium-xchacha20-siv). This algorithm has the advantage that it does not require nonce values, the disadvantage is that duplicate plaintexts will produce duplicate ciphertexts.
Nonces are some extra cryptographic context that is used in many cryptographic algorithms to produce different ciphertexts, even if the plaintexts are the same. The nonce does not have to be secret, but it _does_ have to be unique. **pgsodium** comes with a useful function `pgsodium.crypto_aead_det_noncegen()` that will generate a cryptographically secure nonce for you, and in almost all cases it's best to use that function unless you know specifically what you are doing. In password hashing approaches, this is often similar to how a “salt” value is used to deduplicate password hashes.
Duplicate ciphertexts cannot be used to “attack the key”, it can only reveal the duplication. However, duplication is still information. In our examples so far, an attacker might be able to use this information to determine that two accounts share the same secret. While not technically breaking the encryption, this still leaks information to an attacker.
```sql
create table secrets (
id bigserial primary key,
secret text,
key_id uuid not null default 'e348034b-3f07-4878-aad6-000511d12826'::uuid,
nonce bytea default pgsodium.crypto_aead_det_noncegen()
);
security label for pgsodium
on column secrets.secret
is 'ENCRYPT WITH KEY COLUMN key_id NONCE nonce';
```
This is the most secure form of TCE - there is a unique key ID and a unique nonce per row.
### One Key ID per Row with Associated Data
The encryption that is used for TCE is one of a family of functions provided by **libsodium** to do Authenticated Encryption with Associated Data or [AEAD Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption). The “associated” data is plaintext (unencrypted) information that is mixed into the authentication signature of the encrypted data, such that when you authenticate the data, you also know that the associated data is authentic.
AEAD is helpful because often you have metadata associated with a secret, which isn't confidential but must not be forged.
In our secret example, we might associate a "`secret`" with an "`account_id`". But what if a malicious actor wanted to use someone else's secret on their own account? If someone could forge the `account_id` data column, swapping an `account_id` with their own `account_id`, then you could be tricked into using the wrong secret. By “associating” the `account_id` with the `secret`, it cannot be forged without throwing an error.
Like above, this is done simply by extending the security label with the associated data column:
```sql
create table secrets (
id bigserial primary key,
secret text,
account_id integer,
key_id uuid not null default 'e348034b-3f07-4878-aad6-000511d12826'::uuid,
nonce bytea default pgsodium.crypto_aead_det_noncegen()
);
security label for pgsodium
on column secrets.secret
is 'ENCRYPT WITH KEY COLUMN key_id ASSOCIATED (account_id) NONCE nonce';
```
The new label indicates which column is to be associated with the secret, and that's it! Your `account_id` and secret are now protected under the same authentication signature as the secret itself.
## Using an Encrypted Table
Now that you have TCE setup for a table, it's easy to use by simply inserting data into the table, and querying that data by looking at its generated view. The view is named `decrypted_<table_name>` and by default is in the same schema as your table:
```sql
insert into secrets
(secret, account_id)
values
('1234-5678-8765-4321', 123);
```
Now that you have inserted data, look at the table and notice how the secret is encrypted. This is the data that is stored on disk, the encrypted card number, the key id, and the account id, **but the key itself is not stored**. This means if someone gets a backup or dump of your database, they cannot decrypt the secret, they do not have the key, only the key ID:
```sql
> select * from secrets where account_id = 123;
-[ RECORD 1 ]------+---------------------------------------------------------------------
id | 1
secret | jf8KfImkKTr+j4gzyDZQtLDEFL9eSlFuKjNlNEJvDg+OIKUr2wjF/8NnYcLisb5F9xiN
account_id | 123
key_id | 7f753c4f-8c68-457a-8801-1798b2e9f44d
nonce | \x300a14aa721184ff7cf0f6bf088da267
```
For you, the developer, you need the unencrypted secret for you application. No problem, you can access that data using the dynamically generated decryption view `decrypted_secrets`:
```sql
> select * from decrypted_secrets where account_id = 123;
-[ RECORD 1 ]----------------+---------------------------------------------------------------------
id | 1
secret | jf8KfImkKTr+j4gzyDZQtLDEFL9eSlFuKjNlNEJvDg+OIKUr2wjF/8NnYcLisb5F9xiN
decrypted_secret | 1234-5678-8765-4321
account_id | 123
key_id | 7f753c4f-8c68-457a-8801-1798b2e9f44d
nonce | \x300a14aa721184ff7cf0f6bf088da267
```
Notice how there is a new column called `decrypted_secret`. This column is not stored in database or on disk at all, it is generated “on-the-fly” as you select from the view. Database dumps do not contain this information, only the view itself, and most importantly, **raw decryption keys are never stored**.
- [Supabase Vault](/docs/guides/database/vault)
- Read more about Supabase Vault in the [blog post](https://supabase.com/blog/vault-now-in-beta)
- [Supabase Vault on GitHub](https://github.com/supabase/vault)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -44,12 +44,10 @@ You cannot manage the database schema via the API (for security reasons). To do
You can find the API URL and Keys in the [Dashboard](https://app.supabase.com/project/_/settings/api).
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -63,12 +61,10 @@ Every Supabase project provides a full Postgres database. You can connect to the
2. Click `Database`.
3. Find your Connection Info and Connection String. Direct connections are on port `5432`.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/postgres-connection.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -113,12 +109,10 @@ This is the most granular option. Connections are returned to the pool after eve
2. Click `Database`.
3. Find your Connection Info and Connection String. Connection pooling is on port `6543`.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/connection-pool-config.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -7,7 +7,7 @@ export const meta = {
'pgvector: a PostgreSQL extension for storing embeddings and performing vector similarity search.',
}
[pgvector](https://github.com/pgvector/pgvector/) is a PostgreSQL extension for vector similarity search. It can also be used for storing [embeddings](https://en.wikipedia.org/wiki/Word_embedding).
[pgvector](https://github.com/pgvector/pgvector/) is a PostgreSQL extension for vector similarity search. It can also be used for storing [embeddings](https://supabase.com/blog/openai-embeddings-postgres-vector).
## Concepts
@@ -91,9 +91,12 @@ const { data, error } = await supabase.from('posts').insert({
})
```
## Resources
## More pgvector and Supabase resources
- Source code: [github.com/pgvector/pgvector](https://github.com/pgvector/pgvector/)
- [Supabase Clippy: ChatGPT for Supabase Docs](https://supabase.com/blog/chatgpt-supabase-docs)
- [Storing OpenAI embeddings in Postgres with pgvector](https://supabase.com/blog/openai-embeddings-postgres-vector)
- [A ChatGPT Plugins Template built with Supabase Edge Runtime](https://supabase.com/blog/building-chatgpt-plugins-template)
- [Template for building your own custom ChatGPT style doc search](https://github.com/supabase-community/nextjs-openai-doc-search)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
+1 -7
View File
@@ -22,7 +22,7 @@ You don't have to be a database expert to start using Supabase. Our table view m
Dig into the relationships within your data.
<video width="99%" loop="" muted="" playsInline="" controls="true">
<video width="99%" loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/relational-drilldown-zoom.mp4"
type="video/mp4"
@@ -37,8 +37,6 @@ You can duplicate your tables, just like you would inside a spreadsheet.
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/duplicate-tables.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -50,8 +48,6 @@ Supabase comes with a SQL Editor. You can also save your favorite queries to run
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/favorites.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -79,8 +75,6 @@ You can enable Postgres extensions with the click of a button within the Supabas
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/toggle-extensions.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -20,12 +20,10 @@ Replication is done through _publications_, a method of choosing which changes t
3. Control which database events are sent by toggling **Insert**, **Update**, and **Delete**.
4. Control which tables broadcast changes by selecting **Source** and toggling each table.
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-realtime.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
+1 -3
View File
@@ -41,12 +41,10 @@ and run the SQL queries yourself.
>
<TabPanel id="dashboard" label="Dashboard">
<video width="99%" muted playsInline controls="true">
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-create-table-sm.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
+220
View File
@@ -0,0 +1,220 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'vault',
title: 'Vault',
description: 'Use Supabase to store and serve files.',
sidebar_label: 'Overview',
video: 'https://www.youtube.com/v/J9mTPY8rIXE',
}
Supabase Vault provides encrypted secret storage and Encrypted Columns for Tables.
Vault is a Postgres extension and accompanying Supabase UI that makes it safe and easy to store encrypted secrets and other data in your database. This opens up a lot of possibilities to use Postgres in ways that go beyond what is available in a stock distribution.
From a product perspective, Supabase groups a number of related features under the “Vault banner”. Let's explore a few of these features.
## Secrets Management
Under the hood, the Vault is a table of Secrets and Encryption Keys that are stored using [Authenticated Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption) on disk. They are then available in decrypted form through a Postgres view so that the secrets can be used by applications from SQL. Because the secrets are stored on disk encrypted and authenticated, any backups or replication streams also preserve this encryption in a way that can't be decrypted or forged.
Supabase provides a dashboard UI for the Vault that makes storing secrets easy. Click a button, type in your secret, and save. Optionally create your own keys you can use to encrypt your secret. Your secret will then be stored on disk encrypted using the specified key.
<video width="99%" muted playsInline controls="true">
<source
src="/docs/img/guides/database/vault-hello-compressed.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
There are two main parts to the Vault UI, Secrets and Encryption Keys:
- **Secrets:** Use the Vault to store Secrets - everything from Environment Variables to API Keys. You can use these Secrets anywhere in your database: Postgres [Functions](/docs/guides/database/functions), Triggers, and [Webhooks](/docs/guides/database/webhooks). From a SQL perspective, accessing secrets is as easy as querying a table (or in this case, a view). The underlying secrets tables will be stored in encrypted form.
- **Encryption Keys:** These are keys used to encrypt data inside your database. You can create different Encryption Keys for different purposes, for example: one for encrypting user-data, and another for application-data. Each key is encrypted itself using a root encryption key that lives outside of the database. See **[Encryption key location](#encryption-key-location)** for more details.
## Deep Dive on How The Vault works
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/QHLPNDrdN2w"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
As we mentioned, the Vault uses pgsodium's Transparent Column Encryption (TCE) to store secrets in an authenticated encrypted form. There are some details around that you may be curious about, what does authenticated mean, and where are encryption keys store? This section explains those details.
### Authenticated Encryption with Associated Data
The first important feature of TCE is that it uses an [Authenticated Encryption with Associated Data](<https://en.wikipedia.org/wiki/Authenticated_encryption#Authenticated_encryption_with_associated_data_(AEAD)>) encryption algorithm (based on libsodium).
### Encryption key location
**Authenticated Encryption** means that in addition to the data being encrypted, it is also signed so that it cannot be forged. You can guarantee that the data was encrypted by someone you trust, which you wouldn't get with encryption alone. The decryption function verifies that the signature is valid _before decrypting the value_.
**Associated Data** means that you can include any other columns from the same row as part of the signature computation. This doesn't encrypt those other columns - rather it ensures that your encrypted value is only associated with columns from that row. If an attacker were to copy an encrypted value from another row to the current one, the signature would be rejected (assuming you used a unique column in the associated data).
Another important feature of pgsodium is that the encryption keys are never stored in the database alongside the encrypted data. Instead, only a **Key ID** is stored, which is a reference to the key that is only accessible outside of SQL. Even if an attacker can capture a dump of your entire database, they will see only encrypted data and key IDs, _never the raw key itself_.
This is an important safety precaution - there is little value in storing the encryption key in the database itself as this would be like locking your front door but leaving the key in the lock! Storing the key outside the database fixes this issue.
Where are the keys stored? Supabase creates and manages the root keys (from which all key IDs are derived) in our secured backend systems. We keep this root key safe and separate from your data. You remain in control of your keys - a separate API endpoint is available that you can use to access the key if you want to decrypt your data outside of Supabase.
## Using the Vault
Using the vault is as simple as `INSERT`ing data into the
`vault.secret` table.
```sql
postgres=> insert into vault.secrets (secret) values ('s3kre3t_k3y') returning *;
-[ RECORD 1 ]-------------------------------------------------------------
id | d91596b8-1047-446c-b9c0-66d98af6d001
name |
description |
secret | S02eXS9BBY+kE3r621IS8beAytEEtj+dDHjs9/0AoMy7HTbog+ylxcS22A==
key_id | 7f5ad44b-6bd5-4c99-9f68-4b6c7486f927
nonce | \x3aa2e92f9808e496aa4163a59304b895
created_at | 2022-12-14 02:29:21.3625+00
updated_at | 2022-12-14 02:29:21.3625+00
```
There is also a handy function for creating secrets called
`vault.create_secret()`:
```sql
postgres=> select vault.create_secret('another_s3kre3t');
-[ RECORD 1 ]-+-------------------------------------
create_secret | c9b00867-ca8b-44fc-a81d-d20b8169be17
```
The function returns the UUID of the new secret.
## Name and Description
Secrets can also have an optional _unique_ name, or an optional description. These are also arguments to `vault.create_secret()`:
```sql
postgres=> select vault.create_secret('another_s3kre3t', 'unique_name', 'This is the description');
-[ RECORD 1 ]-+-------------------------------------
create_secret | 7095d222-efe5-4cd5-b5c6-5755b451e223
postgres=> select * from vault.secrets where id = '7095d222-efe5-4cd5-b5c6-5755b451e223';
-[ RECORD 1 ]-----------------------------------------------------------------
id | 7095d222-efe5-4cd5-b5c6-5755b451e223
name | unique_name
description | This is the description
secret | 3mMeOcoG84a5F2uOfy2ugWYDp9sdxvCTmi6kTeT97bvA8rCEsG5DWWZtTU8VVeE=
key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354
nonce | \x9f2d60954ba5eb566445736e0760b0e3
created_at | 2022-12-14 02:34:23.85159+00
updated_at | 2022-12-14 02:34:23.85159+00
```
## Querying Data from the Vault
If you look in the `vault.secrets` table, you will see that your data is stored encrypted. To decrypt the data, there is an automatically created view `vault.decrypted_secrets`. This view will decrypt secret data on the fly:
```sql
postgres=> select * from vault.decrypted_secrets order by created_at desc limit 3;
-[ RECORD 1 ]----+-----------------------------------------------------------------
id | 7095d222-efe5-4cd5-b5c6-5755b451e223
name | unique_name
description | This is the description
secret | 3mMeOcoG84a5F2uOfy2ugWYDp9sdxvCTmi6kTeT97bvA8rCEsG5DWWZtTU8VVeE=
decrypted_secret | another_s3kre3t
key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354
nonce | \x9f2d60954ba5eb566445736e0760b0e3
created_at | 2022-12-14 02:34:23.85159+00
updated_at | 2022-12-14 02:34:23.85159+00
-[ RECORD 2 ]----+-----------------------------------------------------------------
id | c9b00867-ca8b-44fc-a81d-d20b8169be17
name |
description |
secret | a1CE4vXwQ53+N9bllJj1D7fasm59ykohjb7K90PPsRFUd9IbBdxIGZNoSQLIXl4=
decrypted_secret | another_s3kre3t
key_id | 8c72b05e-b931-4372-abf9-a09cfad18489
nonce | \x1d3b2761548c4efb2d29ca11d44aa22f
created_at | 2022-12-14 02:32:50.58921+00
updated_at | 2022-12-14 02:32:50.58921+00
-[ RECORD 3 ]----+-----------------------------------------------------------------
id | d91596b8-1047-446c-b9c0-66d98af6d001
name |
description |
secret | S02eXS9BBY+kE3r621IS8beAytEEtj+dDHjs9/0AoMy7HTbog+ylxcS22A==
decrypted_secret | s3kre3t_k3y
key_id | 7f5ad44b-6bd5-4c99-9f68-4b6c7486f927
nonce | \x3aa2e92f9808e496aa4163a59304b895
created_at | 2022-12-14 02:29:21.3625+00
updated_at | 2022-12-14 02:29:21.3625+00
```
Notice how this view has a `decrypted_secret` column that contains the decrypted secrets. Views are not stored on disk, they are only run at query time, so the secret remains encrypted on disk, and in any backup dumps or replication streams.
You should ensure that you protect access to this view with the appropriate SQL privilege settings at all times, as anyone that has access to the view has access to decrypted secrets.
## Updating Secrets
A secret can be updated with the `vault.update_secret()` function, this function makes updating secrets easy, just provide the secret UUID as the first argument, and then an updated secret, updated optional unique name, or updated description:
```sql
postgres=> select vault.update_secret('7095d222-efe5-4cd5-b5c6-5755b451e223', 'n3w_upd@ted_s3kret',
'updated_unique_name', 'This is the updated description');
-[ RECORD 1 ]-+-
update_secret |
postgres=> select * from vault.decrypted_secrets where id = '7095d222-efe5-4cd5-b5c6-5755b451e223';
-[ RECORD 1 ]----+---------------------------------------------------------------------
id | 7095d222-efe5-4cd5-b5c6-5755b451e223
name | updated_unique_name
description | This is the updated description
secret | lhb3HBFxF+qJzp/HHCwhjl4QFb5dYDsIQEm35DaZQOovdkgp2iy6UMufTKJGH4ThMrU=
decrypted_secret | n3w_upd@ted_s3kret
key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354
nonce | \x9f2d60954ba5eb566445736e0760b0e3
created_at | 2022-12-14 02:34:23.85159+00
updated_at | 2022-12-14 02:51:13.938396+00
```
## Internal Details
To encrypt data, you need a _key id_. You can use the default key id created automatically for every project, or create your own key ids Using the `pgsodium.create_key()` function. Key ids are used to internally derive the encryption key used to encrypt secrets in the vault. Vault users typically do not have access to the key itself, only the key id.
Both `vault.create_secret()` and `vault.update_secret()` take an optional fourth `new_key_id` argument. This argument can be used to store a different key id for the secret instead of the default value.
```sql
postgres=> select vault.create_secret('another_s3kre3t_key', 'another_unique_name',
'This is another description', (pgsodium.create_key()).id);
-[ RECORD 1 ]-+-------------------------------------
create_secret | cec9e005-a44d-4b19-86e1-febf3cd40619
```
Which roles should have access to the `vault.secrets` table should be carefully considered. There are two ways to grant access, the first is that the `postgres` user can explicitly grant access to the vault table itself.
## Turning off Statement Logging
When you insert secrets into the vault table with an INSERT statement, those statements get logged by default into the Supabase logs. Since this would mean your secrets are stored unencrypted in the logs, you should turn off statement logging while using the Vault.
While turning off statement logging does hinder you if you're used to looking at the logs to debug your application, it provides a much higher level of security by ensuring that your data does not leak out of the database and into the logs. This is especially critical with encrypted column data, because the statement logs will contain the _unencrypted_ secrets. If you _must_ store that data encrypted, then you _must_ turn off statement logging.
```sql
alter system set statement_log = 'none';
```
And then restart your project from the dashboard to enable that change.
In the future we are researching various ways to refine the way statement logging interacts with sensitive columns.
## See also
- Read more about Supabase Vault in the [blog post](https://supabase.com/blog/vault-now-in-beta)
- [Supabase Vault on GitHub](https://github.com/supabase/vault)
- [Column Encryption](/docs/guides/database/column-encryption)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
+1
View File
@@ -4,6 +4,7 @@ export const meta = {
id: 'functions',
title: 'Edge Functions',
description: 'Globally distributed TypeScript functions.',
subtitle: 'Globally distributed TypeScript functions.',
sidebar_label: 'Overview',
hide_table_of_contents: true,
}
@@ -27,7 +27,7 @@ Follow the steps to prepare your Supabase project on your local machine.
- Login to the CLI using the command: `supabase login`. [Docs](/docs/reference/cli/usage#supabase-login).
- Initialize Supabase inside your project using the command: `supabase init`. [Docs](/docs/guides/getting-started/local-development#getting-started).
- Link to your Remote Project using the command `supabase link --project-ref your-project-ref`. [Docs](/docs/reference/cli/usage#supabase-link).
- Optional: Setup your environment: Follow [this setup guide](https://deno.land/manual/getting_started/setup_your_environment) to integrate the Deno language server with your editor.
- Setup your environment: Follow [the steps below](/docs/guides/functions/quickstart#setting-up-your-environment).
## Create an Edge Function
@@ -76,6 +76,7 @@ If you receive an error `Invalid JWT`, find the `ANON_KEY` of your project in th
or using one of the [client libraries](/docs#reference-documentation), e.g. using [supabase-js](/docs/reference/javascript/functions-invoke):
```js
// https://supabase.com/docs/reference/javascript/installing
import { createClient } from '@supabase/supabase-js'
// Create a single supabase client for interacting with your database
@@ -88,11 +89,51 @@ const { data, error } = await supabase.functions.invoke('hello-world', {
After invoking your Edge Function you should see the response `{ "message":"Hello Functions!" }`.
## Importing Node npm modules
We recommend using [esm.sh](https://esm.sh/) for importing Node.js modules that are published to npm. To do so you simply put `https://esm.sh/` in front of the package name.
For example, when you want to use supabase-js within Supabase Edge Functions, you would import `createClient` as follows:
```ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
```
As long as your environment is set up properly and the module you're importing is exporting types, the import will have types and autocompletion support.
## Setting Up Your Environment
You can follow the [Deno guide](https://deno.com/manual@v1.32.5/getting_started/setup_your_environment) for setting up your development environment with your favorite editor/IDE.
When developing with VS Code inside of an existing application, you can utilize [multi-root workspaces](https://code.visualstudio.com/docs/editor/workspaces#_multiroot-workspaces).
### Deno with Visual Studio Code (vscode)
Install the Deno language server [via this link](vscode:extension/denoland.vscode-deno) or by browsing the extensions in vscode and choosing to install the _Deno_ extension.
#### Partially Deno enabling a workspace
In a given workspace (or workspace folder), sub-paths can be enabled for Deno, while code outside those paths will be not be enabled and the vscode built-in JavaScript/TypeScript language server will be used.
For example if you have a project like this:
```txt
project
├── app
└── supabase
└── functions
```
Where you only want to enabled the `supabase/functions` path (and its subpaths) to be Deno enabled, you will want to add `./supabase/functions` to the list of _Deno: Enable Paths_ in the configuration. In your `.vscode/settings.json` file add:
```json
{
"deno.enablePaths": ["./supabase/functions"],
"deno.importMap": "./supabase/functions/import_map.json"
}
```
#### Multi-root workspaces
Alternatively, you can utilize [multi-root workspaces](https://code.visualstudio.com/docs/editor/workspaces#_multiroot-workspaces).
<div className="video-container">
<iframe
@@ -143,7 +184,7 @@ For use-cases which require low-latency we recommend [Edge Functions](/docs/guid
We recommend developing “fat functions”. This means that you should develop few large functions, rather than many small functions. One common pattern when developing Functions is that you need to share code between two or more Functions. To do this, you can store any shared code in a folder prefixed with an underscore (`_`). We recommend this folder structure:
```bash
```txt
└── supabase
├── functions
│ ├── import_map.json # A top-level import map to use across functions.
+3 -3
View File
@@ -57,7 +57,7 @@ export const meta = {
<div className="grid lg:grid-cols-12 gap-6 not-prose">
{quickstarts.map((item) => {
return (
<Link href={`/${item.href}`} key={item.title} passHref>
<Link href={`${item.href}`} key={item.title} passHref>
<a className={'col-span-4'}>
<GlassPanel title={item.title} span="col-span-6" background={false} icon={item.icon}>
{item.description}
@@ -73,7 +73,7 @@ export const meta = {
<div className="grid lg:grid-cols-12 gap-6 not-prose">
{webapps.map((item) => {
return (
<Link href={`/${item.href}`} key={item.title} passHref>
<Link href={`${item.href}`} key={item.title} passHref>
<a className={'col-span-4'}>
<GlassPanel title={item.title} span="col-span-6" background={false} icon={item.icon}>
{item.description}
@@ -89,7 +89,7 @@ export const meta = {
<div className="grid lg:grid-cols-12 gap-6 not-prose">
{mobile.map((item) => {
return (
<Link href={`/${item.href}`} key={item.title} passHref>
<Link href={`${item.href}`} key={item.title} passHref>
<a className={'col-span-4'}>
<GlassPanel title={item.title} span="col-span-6" background={false} icon={item.icon}>
{item.description}
@@ -11,8 +11,11 @@ Supabase is open source. We choose open source tools which are scalable and make
Supabase is not a 1-to-1 mapping of Firebase. While we are building many of the features that Firebase offers, we are not going about it the same way:
our technological choices are quite different; everything we use is open source; and wherever possible, we use and support existing tools rather than developing from scratch.
Most notably, we use Postgres rather than a NoSQL store. This choice was deliberate. We believe that no other database offers the functionality required to compete with Firebase,
while maintaining the scalability required to go beyond it.
Most notably, we use Postgres rather than a NoSQL store. This choice was deliberate. We believe that no other database offers the functionality required to compete with Firebase, while maintaining the scalability required to go beyond it.
## Choose your comfort level
Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you just want to treat Postgres like a simple table-store, that’s perfectly fine.
## Architecture
@@ -153,8 +156,7 @@ As the profile of a developer changes over time, Supabase will continue to evolv
### Support existing tools
Supabase supports existing tools and communities wherever possible. Supabase is more like a "community of communities" - each tool typically has its own community
which we work with.
Supabase supports existing tools and communities wherever possible. Supabase is more like a "community of communities" - each tool typically has its own community which we work with.
Open source is something we approach [collaboratively](https://supabase.com/blog/supabase-series-b#giving-back): we employ maintainers, sponsor projects, invest in businesses, and develop our own open source tools.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -6,12 +6,25 @@ export const meta = {
subtitle:
'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.',
breadcrumb: 'OpenAI',
video: 'https://www.youtube.com/v/xmfNUCjszh4',
tocVideo: 'xmfNUCjszh4',
}
In this tutorial we'll look at how you can build a custom ChatGPT-like search experience for your own knowledge base. See our [Supabase Clippy](https://supabase.com/blog/chatgpt-supabase-docs) blog post for an example of how this will look.
We assume that you have a Next.js project with a collection of `.mdx` files nested inside your `pages` directory. We will start developing locally with the Supabase CLI and then push our local database changes to our hosted Supabase project.
## Video Guide
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/xmfNUCjszh4"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<Admonition type="note">
You can find the [full example on
GitHub](https://github.com/supabase-community/nextjs-openai-doc-search).
@@ -458,7 +471,7 @@ All of this is glued together in a [Vercel Edge Function](https://vercel.com/doc
In a last step, we need to process the event stream from the OpenAI API and print the answer to the user. The full code for this can be found on [GitHub](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/components/SearchDialog.tsx).
```tsx
```ts
const handleConfirm = React.useCallback(
async (query: string) => {
setAnswer(undefined)
@@ -87,21 +87,17 @@ export const meta = {
```jsx src/App.jsx
import { createClient } from "@supabase/supabase-js";
import { createEffect, createSignal, For } from "solid-js";
import { createResource, For } from "solid-js";
const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>');
async function getCountries() {
const { data } = await supabase.from("countries").select();
return data;
}
function App() {
const [countries, setCountries] = createSignal();
createEffect(() => {
getCountries();
});
async function getCountries() {
const { data } = await supabase.from("countries").select();
setCountries(data);
}
const [countries] = createResource(getCountries);
return (
<ul>
@@ -85,11 +85,6 @@ export const handle: Handle = async ({ event, resolve }) => {
}
return resolve(event, {
/**
* There´s an issue with `filterSerializedResponseHeaders` not working when using `sequence`
*
* https://github.com/sveltejs/kit/issues/8061
*/
filterSerializedResponseHeaders(name) {
return name === 'content-range'
},
+3 -3
View File
@@ -12,16 +12,16 @@ Explore a variety of integrations from Supabase partners. Need a different integ
<div>
{integrations.items.map((item) => (
<div>
<div key={item.name}>
<h2>{item.name}</h2>
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 not-prose">
{item.items?.map((integration) => (
<Link href={`/${integration.url}`} key={integration.title} passHref>
<Link href={`${integration.url}`} key={integration.name} >
<a>
<GlassPanel
title={integration.name}
background={false}
icon={<Image height={40} width={40} className="rounded" src={`/docs/img/integrations/logos/${integration.name.toLowerCase()}_logo.png`}/>}
icon={<Image height={40} width={40} className="rounded" alt="" src={`/docs/img/integrations/logos/${integration.name.toLowerCase().replace(/ /g,"_")}_logo.png`}/>}
>
{integration.description}
@@ -0,0 +1,108 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'cloudflare-workers',
title: 'Cloudflare Workers',
description:
"Using Supabase from your Cloudflare Workers just got even easier.",
}
Using Supabase in Cloudflare Workers has always been a great way to interact with your data from the edge. Supabase-js communicates with your Supabase Postgres instance via HTTP using PostgREST, so you never need to worry about running out of database connections.
In this guide we'll walk you through a new addition to the Cloudflare Workers dashboard - the ability to authenticate directly with your Supabase account, and automatically inject your Supabase environment variables into your Worker code.
## How To Enable Supabase Integration in Cloudflare Workers
Start by heading to the [Cloudflare Dashboard](https://dash.cloudflare.com), go to the Workers & Pages tab and hit 'Create Application' followed by 'Create Worker'.
![Cloudflare Dashboard 2](/docs/img/guides/integrations/cloudflare-integration/2.png)
Deploy the Hello World example Worker. Once it's deployed hit 'Configure Worker'.
![Cloudflare Dashboard 3](/docs/img/guides/integrations/cloudflare-integration/3.png)
On the configuration page select the Settings tab, followed by the Integrations option.
You should now see the database integration options. On the Supabase card, click 'Add Integration'
![Cloudflare Dashboard 4](/docs/img/guides/integrations/cloudflare-integration/4.png)
After reviewing and accepting the terms, you will be shown the option to connect and a Supabase popup should appear.
Follow the flow by selecting your Supabase Org and the Project you wish to connect to. If you don't have any projects yet, head over to the [Supabase Dashboard](https://app.supabase.com) to create one.
![Cloudflare Dashboard 5](/docs/img/guides/integrations/cloudflare-integration/5.png)
Once it's connected you will be given the option to select which Supabase Key you want to pull into the Worker context.
The `Anon` key here is one that always adhires to the Database's RLS policies (read more on [Row Level Security](https://supabase.com/docs/guides/auth/row-level-security)).
The `Service Role` is typically ok to use in backend contexts, such as Cloudflare Workers, but note that this key **bypasses your Row Level Security policies**, and has the ablity to read, write, and delete any data in your database.
![Cloudflare Dashboard 6](/docs/img/guides/integrations/cloudflare-integration/6.png)
Once this is done the `SUPABASE_KEY` and `SUPABASE_URL` environment variables will now be available from your Cloudflare Worker code.
![Cloudflare Dashboard 7](/docs/img/guides/integrations/cloudflare-integration/7.png)
You can now install the supabase-js client in your Worker:
`npm install @supabase/supabase-js`
Then you can initiate the Supabase client, and start querying your data:
```javascript
import { createClient } from '@supabase/supabase-js'
export default {
async fetch(request, env) {
const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_KEY)
const { data, error } = await supabase.from('countries').select('*')
if (error) throw error
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
},
})
},
}
```
The snippet above assumes you already have a `countries` table. Run the following in the [SQL Editor in the Supabase Dashboard](https://app.supabase.com/project/_/sql) if you wish to install this demo schema:
```sql
create table countries (
id serial primary key,
name varchar(255) not null
);
insert into countries
(name)
values
('Oceania');
insert into countries
(name)
values
('Genovia');
insert into countries
(name)
values
('Wakanda');
insert into countries
(name)
values
('Lilliput');
```
Remember that you don't need to use supabase-js to connect to your Supabase database, you can connect "directly" to the underlying Postgres database using the connection string (every Supabase database comes pre-installed with a [connection pooler](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool)), or you can try Cloudflare's new [TCP socket method of connecting to Postgres](https://blog.cloudflare.com/workers-tcp-socket-api-connect-databases/) directly from Cloudflare Workers.
- [Cloudflare Integration Docs](https://developers.cloudflare.com/workers/learning/integrations/databases/#supabase).
- [Cloudflare Dashboard](dash.cloudflare.com/).
- [Cloudflare Integration Announcement](https://blog.cloudflare.com/announcing-database-integrations/).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -46,8 +46,6 @@ You can then connect your Supabase project to your FlutterFlow project with the
<source
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/connect-flutterflow-to-supabase.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -69,8 +67,6 @@ Go to your project page on FlutterFlow and follow the steps below to define the
<source
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/insert.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -90,8 +86,6 @@ To query a Supabase table on a ListView:
<source
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/select.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -134,8 +128,6 @@ Go to your project page on FlutterFlow and follow the steps below to define the
<source
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/delete.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -104,12 +104,10 @@ To tell Plasmic to look for your code components on your dev server, you’ll ne
> Note: At this point, you’ll need to keep your dev server running at `http://localhost:3000`for the project to load.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/plasmic/configure-app-host-plasmic-05.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -121,12 +119,10 @@ Let’s make a visual gallery for our Pokemon by using the code components from
Create a new page called `Gallery`, and set a path for this page (`/gallery`).
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/plasmic/create-gallery-page-plasmic-06.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -154,12 +150,10 @@ Now customize the repeated content by inserting instances of `SupabaseField`. Se
For your convenience, the following video shows you how to create the page end-to-end.
<video width="99%" autoPlay="autoplay" muted playsInline controls={true}>
<video width="99%" autoPlay muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/plasmic/end-to-end-plasmic-10.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
@@ -36,8 +36,8 @@ If you are transferring ownership of your organization to someone else, you will
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 | Read only [^2] |
| ------------------------ | ----------------------- | ----------------------- | ----------------------- | ----------------------- |
| **Organization** |
| Change organization name | <IconCheck size={14} /> | | |
| Delete organization | <IconCheck size={14} /> | | |
@@ -70,6 +70,8 @@ The table below shows the corresponding permissions for each available role you
| 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} /> |
| Manage tables | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
| View Data | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> | <IconCheck size={14} /> |
[^1]:
Invites sent from a SSO account can only be accepted by another SSO account
@@ -77,6 +79,8 @@ The table below shows the corresponding permissions for each available role you
prevents accidental invites to accounts not managed by your company's
enterprise systems.
[^2]: Available on the Teams and Enterprise Tiers.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -10,16 +10,16 @@ Every project on the Supabase Platform comes with its own dedicated Postgres ins
| Plan | Pricing | CPU | Memory | Maximum Disk IO Bandwidth | Baseline Disk IO Bandwidth | Connections: Direct (recommended) | Connections: Pooler (recommended) |
| --------------- | ------- | ----------------------- | ------ | ------------------------- | -------------------------- | --------------------------------- | --------------------------------- |
| Free (Included) | $0 | 2-core ARM (shared) | 1 GB | 2,606 Mbps | 87 Mbps | 10 | 50 |
| Small | $5 | 2-core ARM (shared) | 2 GB | 2,606 Mbps | 174 Mbps | 30 | 75 |
| Medium | $50 | 2-core ARM (shared) | 4 GB | 2,606 Mbps | 347 Mbps | 50 | 150 |
| Large | $100 | 2-core ARM (dedicated) | 8 GB | 4,750 Mbps | 630 Mbps | 100 | 300 |
| XL | $200 | 4-core ARM (dedicated) | 16 GB | 4,750 Mbps | 1,188 Mbps | 200 | 600 |
| 2XL | $400 | 8-core ARM (dedicated) | 32 GB | 4,750 Mbps | 2,375 Mbps | 350 | 1200 |
| 4XL | $950 | 16-core ARM (dedicated) | 64 GB | 4,750 Mbps | 4,750 Mbps | 420 | 2800 |
| 8XL | $1,860 | 32-core ARM (dedicated) | 128 GB | 9,500 Mbps | 9,500 Mbps | 450 | 5600 |
| 12XL | $2,790 | 48-core ARM (dedicated) | 192 GB | 14,250 Mbps | 14,250 Mbps | 480 | 8600 |
| 16XL | $3,720 | 64-core ARM (dedicated) | 256 GB | 19,000 Mbps | 19,000 Mbps | 500 | 11,600 |
| Free (Included) | $0 | 2-core ARM (shared) | 1 GB | 2,606 Mbps | 87 Mbps | 60 | 200 |
| Small | $5 | 2-core ARM (shared) | 2 GB | 2,606 Mbps | 174 Mbps | 90 | 200 |
| Medium | $50 | 2-core ARM (shared) | 4 GB | 2,606 Mbps | 347 Mbps | 120 | 200 |
| Large | $100 | 2-core ARM (dedicated) | 8 GB | 4,750 Mbps | 630 Mbps | 160 | 300 |
| XL | $200 | 4-core ARM (dedicated) | 16 GB | 4,750 Mbps | 1,188 Mbps | 240 | 700 |
| 2XL | $400 | 8-core ARM (dedicated) | 32 GB | 4,750 Mbps | 2,375 Mbps | 380 | 1500 |
| 4XL | $950 | 16-core ARM (dedicated) | 64 GB | 4,750 Mbps | 4,750 Mbps | 480 | 3000 |
| 8XL | $1,860 | 32-core ARM (dedicated) | 128 GB | 9,500 Mbps | 9,500 Mbps | 490 | 6000 |
| 12XL | $2,790 | 48-core ARM (dedicated) | 192 GB | 14,250 Mbps | 14,250 Mbps | 500 | 9000 |
| 16XL | $3,720 | 64-core ARM (dedicated) | 256 GB | 19,000 Mbps | 19,000 Mbps | 500 | 12,000 |
[Contact us](https://supabase.com/contact/enterprise) if you require a custom plan.
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'going-into-prod',
title: 'Production Readiness',
title: 'Production Checklist',
description: 'Things to do before making your app publicly available',
}
@@ -35,6 +35,7 @@ After developing your project and deciding it's Production Ready, you should run
- Tools like [k6](https://k6.io/) can simulate traffic from many different users.
- Upgrade your database if you require more resources. If you need anything beyond what is listed, contact enterprise@supabase.io.
- If you are expecting a surge in traffic (for a big launch) [contact support](https://app.supabase.com/support/new) with more details about your launch. We'll keep an eye on your project.
- If you expect your database size to be > 4 GB, [enable](https://app.supabase.com/project/_/settings/billing/update) the Point in Time Recovery (PITR) addon. Daily backups can take up resources from your database when the backup is in progress. PITR is more resource efficient, since only the changes to the database are backed up.
## Availability
@@ -47,6 +48,7 @@ After developing your project and deciding it's Production Ready, you should run
- Database backups are not available for download on the free tier.
- You can set up your own backup systems using tools like [pg_dump](https://www.postgresqltutorial.com/postgresql-backup-database/) or [wal-g](https://github.com/wal-g/wal-g).
- Nightly backups for Pro tier projects are available on the Supabase dashboard for up to 7 days.
- 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. 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 a lower RPO is required, enable PITR.
- Upgrading to the Supabase Pro Tier will give you [access to our support team](https://app.supabase.com/support/new).
## Rate Limiting, Resource Allocation, & Abuse Prevention
+22 -1
View File
@@ -112,7 +112,28 @@ To _permanently_ set a logging configuration (beyond a single session), execute
alter role postgres set pgaudit.log to 'function, write, ddl';
```
For user-traffic flowing through the [HTTP APIs](../../guides/database/api#rest-api-overview) powered by PostgREST, set your configuration values for the roles `anon` and `authenticated`. For traffic using the `service_role` key, set the configuration values for the role `service_role`.
To help with debugging, we recommend adjusting the log scope to only relevant statements as having too wide of a scope would result in a lot of noise in your Postgres logs.
Note that in the above example, the role is set to `postgres`. To log user-traffic flowing through the [HTTP APIs](../../guides/database/api#rest-api-overview) powered by PostgREST, set your configuration values for the roles `anon` and `authenticated`. For traffic using the `service_role` key, set the configuration values for the role `service_role`.
```sql
-- for API-related logs
alter role anon set pgaudit.log to 'write';
alter role authenticated set pgaudit.log to 'write';
-- for service role logs
alter role service_role set pgaudit.log to 'write';
```
By default, the log level will be set to `log`. To view other levels, run the following:
```sql
-- adjust log level
alter role postgres set pgaudit.log_level to 'info';
alter role postgres set pgaudit.log_level to 'debug5';
```
Note that as per the pgAudit [log_level documentation](https://github.com/pgaudit/pgaudit/blob/master/README.md#pgauditlog_level), `error`, `fatal`, and `panic` are not allowed.
To reset system-wide settings, execute the following, then perform a fast reboot:
@@ -0,0 +1,49 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'maturity-model',
title: 'Maturity Model',
description: 'Growing with your application.',
}
Supabase is great for building something very fast _and_ for scaling up. However, it's important to note that as your application matures and your team expands, the practices you use for managing an application in production should not be the same as the practices you used for prototyping.
## Prototyping
The Dashboard is a quick and easy tool for building applications while you are prototyping. That said, we strongly recommend using [Migrations](/docs/guides/getting-started/local-development#database-migrations) to manage your database changes. You can use our CLI to [capture any changes](/docs/reference/cli/supabase-db-diff) you have made on the Dashboard so that you can commit them a version control system, like git.
## Collaborating
As soon as you start collaborating with team members, all project changes should be in version control. At this point we strongly recommend moving away from using the Dashboard for schema changes. Use migrations to manage your database, and check them into your version control system to track every change.
Resources:
- [Database migrations](/docs/guides/getting-started/local-development#database-migrations)
- [Managing access on the Dashboard](/docs/guides/platform/access-control)
- [PGAudit for Postgres](/docs/guides/database/extensions/pgaudit)
## In Production
Once your application is live, you should never change your database using the Dashboard. Supabase provides various [access levels](https://supabase.com/docs/guides/platform/access-control) for the Dashboard that can help enforce this. Some other important things to consider at this point include:
- Read the [Production Checklist](docs/guides/platform/going-into-prod).
- Make your team aware of the [Shared Responsibilities](/docs/guides/platform/shared-responsibility-model) between your organization and Supabase.
- Design a [safe workflow](https://supabase.com/docs/guides/platform/shared-responsibility-model#you-decide-your-own-workflow) for managing your database. We strongly recommend running [multiple environments](/docs/guides/cli/managing-environments) as part of your development workflow (`local` -> `staging` -> `prod`).
- As your database to grows, we strongly recommend moving to [Point-in-Time Recovery](/docs/guides/platform/backups#point-in-time-recovery). This is safer and has less impact on your database performance during maintenance windows.
Resources:
- [Database migrations](/docs/guides/getting-started/local-development#database-migrations)
- [Managing access on the Dashboard](/docs/guides/platform/access-control)
- [PGAudit for Postgres](/docs/guides/database/extensions/pgaudit)
- [Managing environments](/docs/guides/cli/managing-environments)
## Enterprise
For a more secure setup, consider running your workload across several organizations. It's a common pattern to have a Production organization which is restricted to only those team members who are qualified to have direct access to production databases.
Reach out to [growth@supabase.com](mailto:growth@supabase.com) if you need help designing a secure development workflow for your organization.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -49,7 +49,6 @@ In your new project:
1. Enable [Database Webhooks](https://app.supabase.com/project/_/database/hooks) if you enabled them in your old project.
2. Enable any [extensions](https://app.supabase.com/project/_/database/extensions) that were enabled in your old project.
3. Create any [custom roles](https://app.supabase.com/project/_/database/roles) that were created in your old project.
Then run the following command from your terminal:
@@ -57,11 +56,14 @@ Then run the following command from your terminal:
psql \
--single-transaction \
--variable ON_ERROR_STOP=1 \
--file roles.sql \
--file schema.sql \
--file data.sql \
--dbname "$NEW_DB_URL"
```
Note if you have created any [custom roles](https://app.supabase.com/project/_/database/roles) with `login` attribute, you have to manually set their passwords in the new project.
### Enable publication on tables
Replication for Realtime is disabled for all tables in your new project. On the [Replication](https://app.supabase.com/project/_/database/replication) page in the Dashboard, select your new project and enable replication for tables that were enabled in your old project.
@@ -0,0 +1,51 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'shared-responsibility-model',
title: 'Shared Responsibility Model',
description: 'Running databases is a shared responsibility between you and Supabase.',
}
Running databases is a shared responsibility between you and Supabase. There are some things that we can take care of for you, and some things that you are responsible for. This is by design: we to give you the freedom to use your database however you want. While we _could_ put many more restrictions in place to ensure that you can’t do anything wrong, you will eventually find those restrictions prohibitive.
Generally, we aim reduce your burden of managing infrastructure and knowing about Postgres internals, minimizing configuration as much as we can. Here are a few things that you should know:
## You share the security responsibility
We give you full access to the database. If you share that access with other people (either people on your team, or the public in general) then it is your responsibility to ensure that the access levels you provide are correctly managed.
If you have an inexperienced member on your team, then you probably shouldn’t give them access to Production. You should set internal workflows around what they should and should not be able to do, with restricted access to avoid anything that might be deemed dangerous.
You are also responsible for ensuring that tables with sensitive data have the right level of access. You are also responsible for managing your database secrets and API keys, storing them safely in an encrypted store.
## You decide your own workflow
There are _many_ ways to work with Supabase.
You can use our Dashboard, our client libraries, external tools like Prisma and Drizzle, or migration tools like our CLI, Flyway, Sqitch, and anything else that is Postgres-compatible. You can develop directly on your database while you're getting started, run migrations from [local to production](/docs/guides/getting-started/local-development), or you can use [multiple environments](/docs/guides/cli/managing-environments).
None of these are right or wrong. It depends on the stage of your project. You _definitely_ shouldn’t be developing on your database directly when you’re in production - but that’s absolutely fine when you’re prototyping and don’t have users.
## You are responsible for your application architecture
Supabase isn't a silver-bullet for bad architectural decisions. A poorly designed database will run poorly, no matter where it’s hosted.
You can get away with a poorly-designed database for a while by simply adding compute. After a while, things will start to break. The database schema is the area you want to spend _the most_ time thinking about. That’s the benefit of Supabase - you can spend more time designing a scalable database system and less time thinking about the mundane tasks like implementing CRUD APIs.
If you don’t want to implement logic inside your database, that is 100% fine. You can use _any_ tools which work with Postgres.
## You choose your level of comfort with Postgres
Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you just want to treat Postgres like a simple table-store, that’s perfectly fine.
## You are in control of your database
Supabase places very few guard-rails around your database. That gives you a lot of control, but it also means you can break things. ”Break” is used liberally here. It refers to any situation that affects your application because of the way you're using the database.
You are responsible for using best-practices to optimize and manage your database: adding indexes, adding filters on large queries, using caching strategies, optimizing your database queries, and managing connections to the database.
You are responsible of provisioning enough compute to run the workload that your application requires. The Supabase Dashboard provides [observability tooling](https://app.supabase.com/project/_/reports/database) to help with this.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -93,5 +93,12 @@ pgloader config.load
</TabPanel>
</Tabs>
<Admonition type="caution">
- If you plan on migrating a database larger than 6 GB, we recommend [contacting support](https://app.supabase.com/support/new) to ensure that you'll have the proper disk size pre-provisioned. You can read more about how the disk is managed on Supabase on the [Database usage](/docs/guides/platform/database-size#disk-management).
- We also recommend upgrading to at least a [Large instance](/docs/guides/platform/compute-add-ons) for the migration (you can downgrade later) to avoid running out of IO-Budget.
</Admonition>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -67,6 +67,13 @@ psql -h $SUPABASE_HOST -U postgres -f heroku_dump.sql
Run `pg_dump --help` for a full list of options.
<Admonition type="caution">
- If you plan on migrating a database larger than 6 GB, we recommend [contacting support](https://app.supabase.com/support/new) to ensure that you'll have the proper disk size pre-provisioned. You can read more about how the disk is managed on Supabase on the [Database usage](/docs/guides/platform/database-size#disk-management).
- We also recommend upgrading to at least a [Large instance](/docs/guides/platform/compute-add-ons) for the migration (you can downgrade later) to avoid running out of IO-Budget.
</Admonition>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -83,6 +83,13 @@ Run `pg_dump --help` for a full list of options.
</TabPanel>
</Tabs>
<Admonition type="caution">
- If you plan on migrating a database larger than 6 GB, we recommend [contacting support](https://app.supabase.com/support/new) to ensure that you'll have the proper disk size pre-provisioned. You can read more about how the disk is managed on Supabase on the [Database usage](/docs/guides/platform/database-size#disk-management).
- We also recommend upgrading to at least a [Large instance](/docs/guides/platform/compute-add-ons) for the migration (you can downgrade later) to avoid running out of IO-Budget.
</Admonition>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
+1 -1
View File
@@ -134,7 +134,7 @@ secrets manager when deploying to production. Plain text files like dotenv lead
Some suggested systems include:
- [Doppler](https://www.doppler.com/)
- [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/general/overview) by Azure
- [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/general/overview) by Azure (Microsoft)
- [Secrets Manager](https://aws.amazon.com/secrets-manager/) by AWS
- [Secrets Manager](https://cloud.google.com/secret-manager) by GCP
- [Vault](https://www.hashicorp.com/products/vault) by Hashicorp
+263 -271
View File
@@ -1,224 +1,199 @@
import Layout from '~/layouts/DefaultLayout'
import HomeLayout from '~/layouts/HomeLayout'
import Link from 'next/link'
import { GlassPanel, IconPanel, Button, IconChevronRight } from 'ui'
import {
Button,
GlassPanel,
IconBackground,
IconChevronRight,
IconPanel,
IconPlay,
TextLink,
} from 'ui'
import HomeMenuIconPicker from '~/components/Navigation/NavigationMenu/HomeMenuIconPicker'
export const meta = {
title: 'Supabase Docs',
title: 'Supabase Documentation',
description:
'Supabase is an open source Firebase alternative providing all the backend features you need to build a product.',
subtitle: 'Supabase Developer Documentation and API Reference',
}
<h2 className="text-base max-w-xl m-0">
Supabase is an open source Firebase alternative providing all the backend features you need to
build a product. [Learn more](/docs/guides/getting-started/architecture) about Supabase, follow a
quickstart for an overview, or dive straight into the different products and APIs.
</h2>
<div className="flex flex-col">
{/* start container */}
<div>
<div className="max-w-xl">
## Products
</div>
<div className="flex flex-col gap-12 my-12">
<div className="grid grid-cols-12 gap-6 not-prose [&_svg]:text-brand-1100 [&_svg]dark:text-brand-900">
{products.map((product) => {
return (
<Link href={product.href} key={product.title} passHref>
<a className={product.span ?? 'col-span-12 md:col-span-4'}>
<GlassPanel {...product} icon={<HomeMenuIconPicker icon={product.icon} width={18} height={18} />} background={true} showIconBg={true} showLink={false}>
{product.description}
</GlassPanel>
</a>
</Link>
)
})}
</div>
<div className="space-y-8">
<div className="
border border-scale-400
relative overflow-hidden
grid grid-cols-12
rounded-lg
px-8 md:px-12 py-5 md:py-12
">
<div className="col-span-12 lg:col-span-7 flex flex-col gap-6">
<img className="hidden lg:visible absolute right-0 top-0 w-[500px]" src="/docs/img/cards/dark.png" />
<div className="">
<h3 className="text-2xl m-0 mb-3">Getting Started</h3>
<div className="flex flex-col lg:grid grid-cols-12 gap-6 py-12 border-b">
Discover how to set up a database to an app making queries in just a few minutes.
<div className="col-span-4 flex flex-col gap-1 [&_h2]:m-0 [&_h3]:m-0">
<div className="font-mono uppercase text-xs mb-2">What you will learn</div>
<div className="flex flex-col">
<div className="flex flex-row items-center gap-3">
<div className="text-brand-900">
<IconChevronRight size={14} strokeWidth={1.5} />
</div>
<span className="text-scale-1200">Setting up a Supabase project and app</span>
</div>
<div className="flex flex-row items-center gap-3">
<div className="text-brand-900">
<IconChevronRight size={14} strokeWidth={1.5} />
</div>
<span className="text-scale-1200">Adding some sample data to your project</span>
</div>
<div className="flex flex-row items-center gap-3">
<div className="text-brand-900">
<IconChevronRight size={14} strokeWidth={1.5} />
</div>
<span className="text-scale-1200">Select() from your new database table</span>
<div className="md:max-w-xs 2xl:max-w-none">
<div className="flex items-center gap-3 mb-3 text-brand-1100 dark:text-brand-900">
{/* <IconBackground>
<HomeMenuIconPicker icon="reference-cli" width={18} height={18} />
</IconBackground> */}
## Client Libraries
</div>
</div>
<div className="mt-6">
<div className="hidden">
<Button size="small">Get started</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-6">
<Link href={`/guides/getting-started/quickstarts/reactjs`} passHref>
<a className="no-underline">
<IconPanel title="ReactJS" icon="/docs/img/icons/react-icon" />
</div>
<div className="grid col-span-8 grid-cols-12 gap-6 not-prose">
{clientLibraries.map((product) => {
return (
<Link href={product.href} key={product.title} passHref>
<a className={product.span ?? 'col-span-6 md:col-span-4'}>
<IconPanel
{...product}
icon={<HomeMenuIconPicker icon={product.icon} width={18} height={18} />}
background={true}
showIconBg={true}
showLink={false}
>
{product.description}
</IconPanel>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/nextjs`} passHref>
<a className="no-underline">
<IconPanel title="NextJS" icon="/docs/img/icons/nextjs-icon" />
)
})}
</div>
</div>
<div className="flex flex-col lg:grid grid-cols-12 gap-6 py-12 border-b">
<div className="col-span-4 flex flex-col gap-1 [&_h2]:m-0">
## Migrate to Supabase
<p className="text-scale-1100 text-sm p-0 m-0">
Bring your existing data, auth and storage to Supabase following our migration guides.
</p>
<TextLink
label="Explore more resources"
url="/docs/guides/resources"
className="no-underline text-brand-900 text-sm"
/>
</div>
<div className="grid col-span-8 grid-cols-12 gap-6 not-prose">
{migrationGuides.map((product) => {
return (
<Link
href={product.href}
key={product.title}
passHref
>
<a className={product.span ?? 'col-span-6 md:col-span-4'}>
<IconPanel {...product} background={true} showIconBg={true} showLink={false}>
{product.description}
</IconPanel>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/redwoodjs`} passHref>
<a className="no-underline">
<IconPanel title="RedwoodJS" icon="/docs/img/icons/redwoodjs-icon" />
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/flutter`} passHref>
<a className="no-underline">
<IconPanel title="Flutter" icon="/docs/img/icons/flutter-icon" />
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/sveltekit`} passHref>
<a className="no-underline">
<IconPanel title="SvelteKit" icon="/docs/img/icons/svelte-icon" />
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/solidjs`} passHref>
<a className="no-underline">
<IconPanel title="SolidJS" icon="/docs/img/icons/solidjs-icon" />
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/vue`} passHref>
<a className="no-underline">
<IconPanel title="Vue" icon="/docs/img/icons/vuejs-icon" />
</a>
</Link>
</div>
)
})}
</div>
</div>
</div>
<div className="flex flex-col gap-6 py-12 border-b">
<div className="col-span-4 flex flex-col gap-1 [&_h2]:m-0 [&_h3]:m-0">
### Additional resources
</div>
<div className="grid grid-cols-12 gap-6 not-prose">
{additionalResources.map((product) => {
return (
<Link
href={product.href}
key={product.title}
passHref
>
<a className={product.span ?? 'col-span-12 md:col-span-6 lg:col-span-3'}>
<GlassPanel {...product} icon={<HomeMenuIconPicker icon={product.icon} width={18} height={18} />} background={false}>
{product.description}
</GlassPanel>
</a>
</Link>
)
})}
</div>
</div>
<div className="flex flex-col lg:grid grid-cols-12 gap-6 py-12">
<div className="col-span-4 flex flex-col gap-1 [&_h2]:m-0 [&_h3]:m-0">
<div className="md:max-w-xs 2xl:max-w-none">
<div className="flex items-center gap-3 mb-3 text-brand-1100 dark:text-brand-900">
<IconBackground>
<HomeMenuIconPicker icon="self-hosting" width={18} height={18} />
</IconBackground>
### Self-Hosting
</div>
<p className="text-scale-1100 text-sm">
Get started with self-hosting Supabase.
</p>
<TextLink
label="More on Self-Hosting"
url="/docs/guides/self-hosting"
className="no-underline text-brand-900 text-sm"
/>
</div>
</div>
<div className="grid col-span-8 grid-cols-12 gap-6 not-prose">
<div className="col-span-full lg:col-span-8 grid grid-cols-12 gap-6">
{selfHostingOptions.map((product) => {
return (
<Link
href={product.href}
key={product.title}
passHref
>
<a className={product.span ?? 'col-span-6'}>
<IconPanel {...product} icon={<HomeMenuIconPicker icon={product.icon} width={18} height={18} />} background={true} showIconBg={true} showLink={false}>
{product.description}
</IconPanel>
</a>
</Link>
)
})}
</div>
</div>
</div>
<div
className="
border
relative
px-8 md:px-12 h-20
border-scale-400
overflow-hidden
min-w-full
rounded-lg
flex flex-row gap-6 items-center
"
>
<img
className="w-[48px] lg:w-[64px]"
src="/docs/img/cards/tutorials-small.svg"
/>
<img
className="z-0 absolute -left-32 -bottom-48 w-[320px]"
src="/docs/img/purple-glow-dark.png"
/>
<div className="
flex flex-col md:flex-row not-prose
h-full justify-center
md:items-center md:gap-3
">
<h2 className="text-sm lg:text-base text-scale-1200 m-0">Ready to make a fully working app?</h2>
<p className="text-sm lg:text-base text-scale-1100 m-0">Follow the step-by-step <Link href="/guides/getting-started#web-app-tutorials"><a className="m-0 text-scale-1200 underline">tutorials</a></Link>.</p>
</div>
</div>
</div>
<div>
<div className="max-w-xl">
### Product Documentation
</div>
<div className="grid grid-cols-12 gap-6 not-prose">
{products.map((product) => {
return (
<Link
href={`${product.href}`}
key={product.title}
passHref
>
<a className={product.span ?? 'col-span-12 md:col-span-4'}>
<GlassPanel {...product} background={true} showIconBg={true} showLink={true}>
{product.description}
</GlassPanel>
</a>
</Link>
)
})}
{platform.map((product) => {
return (
<Link
href={`/${product.href}`}
key={product.title}
passHref
>
<a className={'col-span-12 md:col-span-6'}>
<GlassPanel {...product} background={false} showIconBg={true}>
{product.description}
</GlassPanel>
</a>
</Link>
)
})}
</div>
</div>
<div>
<div className="max-w-xl">
### Reference Documentation
</div>
<div className="grid grid-cols-12 gap-6 not-prose">
{apis.map((product) => {
return (
<Link
href={`/${product.href}`}
key={product.title}
passHref
>
<a className={product.span ?? 'col-span-12 lg:col-span-4'}>
<GlassPanel {...product} background={false}>
{product.description}
</GlassPanel>
</a>
</Link>
)
})}
</div>
</div>
{/* end of container */}
</div>
export const products = [
{
title: 'Database',
icon: '/docs/img/icons/menu/database',
icon: 'database',
hasLightIcon: true,
href: '/guides/database',
description:
@@ -227,16 +202,16 @@ export const products = [
},
{
title: 'Auth',
icon: '/docs/img/icons/menu/auth',
icon: 'auth',
hasLightIcon: true,
href: '/guides/auth/overview',
href: '/guides/auth',
description:
'Add and manage email and password, passwordless, OAuth, and mobile logins to your project through a suite of identity providers and APIs.',
span: 'col-span-12 md:col-span-6',
},
{
title: 'Storage',
icon: '/docs/img/icons/menu/storage',
icon: 'storage',
hasLightIcon: true,
href: '/guides/storage',
description:
@@ -244,7 +219,7 @@ export const products = [
},
{
title: 'Realtime',
icon: '/docs/img/icons/menu/realtime',
icon: 'realtime',
hasLightIcon: true,
href: '/guides/realtime',
description:
@@ -252,7 +227,7 @@ export const products = [
},
{
title: 'Edge Functions',
icon: '/docs/img/icons/menu/functions',
icon: 'functions',
hasLightIcon: true,
href: '/guides/functions',
description:
@@ -260,105 +235,122 @@ export const products = [
},
]
export const apis = [
export const migrationGuides = [
{
title: 'JavaScript Client Library',
icon: '/docs/img/icons/menu/reference-javascript',
hasLightIcon: true,
title: 'Firebase Auth',
icon: '/docs/img/icons/firebase-icon',
href: '/guides/resources/migrating-to-supabase/firebase-auth',
},
{
title: 'Firestore Data',
icon: '/docs/img/icons/firebase-icon',
href: '/guides/resources/migrating-to-supabase/firestore-data',
},
{
title: 'Firebase Storage',
icon: '/docs/img/icons/firebase-icon',
href: '/guides/resources/migrating-to-supabase/firebase-storage',
},
{
title: 'Heroku',
icon: '/docs/img/icons/heroku-icon',
href: '/guides/resources/migrating-to-supabase/heroku',
},
{
title: 'Render',
icon: '/docs/img/icons/render-icon',
href: '/guides/resources/migrating-to-supabase/render',
},
{
title: 'Amazon RDS',
icon: '/docs/img/icons/aws-rds-icon',
href: '/guides/resources/migrating-to-supabase/amazon-rds',
},
]
export const selfHostingOptions = [
{
title: 'Auth',
icon: 'auth',
href: 'reference/self-hosting-auth/introduction',
},
{
title: 'Realtime',
icon: 'realtime',
href: 'reference/self-hosting-realtime/introduction',
},
{
title: 'Storage',
icon: 'storage',
href: 'reference/self-hosting-storage/introduction',
},
{
title: 'Analytics',
icon: 'analytics',
href: 'reference/self-hosting-analytics/introduction',
},
]
export const clientLibraries = [
{
title: 'Javascript',
icon: 'reference-javascript',
href: 'reference/javascript/introduction',
description: 'Official client libraries for JavaScript and TypeScript.',
span: 'col-span-12 md:col-span-6',
},
{
title: 'Flutter Client Library',
icon: '/docs/img/icons/menu/reference-dart',
hasLightIcon: true,
title: 'Flutter',
icon: 'reference-dart',
href: 'reference/dart/introduction',
description: 'Official client libraries for Flutter and Dart.',
span: 'col-span-12 md:col-span-6',
},
{
title: 'Python',
icon: 'reference-python',
href: 'reference/python/introduction',
},
{
title: 'C#',
icon: 'reference-csharp',
href: 'reference/csharp/introduction',
},
{
title: 'Swift',
icon: 'reference-swift',
href: 'reference/swift/introduction',
},
{
title: 'Kotlin',
icon: 'reference-kotlin',
href: 'reference/kotlin/introduction',
},
]
export const additionalResources = [
{
title: 'Management API',
icon: '/docs/img/icons/menu/reference-api',
hasLightIcon: true,
description: 'Manage your Supabase projects and organizations.',
icon: 'reference-api',
href: 'reference/api/introduction',
description: 'Manage your Supabase projects and organizations programmatically.',
span: 'col-span-12 md:col-span-6',
},
{
title: 'Supabase CLI',
icon: '/docs/img/icons/menu/reference-api',
hasLightIcon: true,
description: 'Use the CLI to develop, manage and deploy your projects.',
icon: 'reference-cli',
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',
icon: '/docs/img/icons/menu/reference-auth',
hasLightIcon: true,
href: 'reference/self-hosting-auth/introduction',
description: 'Self-host your own Auth server.',
},
{
title: 'Self-Hosting Storage',
icon: '/docs/img/icons/menu/reference-storage',
hasLightIcon: true,
href: 'reference/self-hosting-storage/introduction',
description: 'Self-host your own Storage server.',
},
{
title: 'Self-Hosting Realtime',
icon: '/docs/img/icons/menu/reference-realtime',
hasLightIcon: true,
href: 'reference/self-hosting-realtime/introduction',
description: 'Self-host your own Realtime server.',
},
{
title: 'Self-Hosting Analytics',
icon: '/docs/img/icons/menu/reference-analytics',
hasLightIcon: true,
href: 'reference/self-hosting-analytics/introduction',
description: 'Self-host your own Analytics server.',
},
]
export const platform = [
{
// header: '/docs/img/cards/sample-card-header-3.svg',
title: 'Platform Guides',
icon: '/docs/img/icons/menu/platform',
hasLightIcon: true,
description: 'Learn more about the tools and services powering Supabase.',
icon: 'platform',
href: 'guides/platform',
description:
'Learn more about the tools and services powering the Supabase Platform, available add-ons, and how to customize the platform for your needs.',
},
{
// header: '/docs/img/cards/sample-card-header-2.svg',
title: 'Resources',
icon: '/docs/img/icons/menu/resources',
hasLightIcon: true,
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',
icon: '/docs/img/icons/menu/integrations',
hasLightIcon: true,
href: 'guides/integrations',
description: 'Explore a variety of integrations from Supabase partners.',
icon: 'integrations',
href: 'guides/integrations',
},
]
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export const Page = ({ children }) => <HomeLayout meta={meta} children={children} />
export default Page
+1 -1
View File
@@ -25,7 +25,7 @@ export default function Config() {
<Head>
<title>Supabase CLI config</title>
</Head>
<div className="grid grid-cols-12 relative gap-4">
<div className="grid grid-cols-12 relative gap-4 px-5 max-w-7xl mx-auto py-16">
<div className="relative col-span-12 md:col-span-9 transition-all ease-out duration-100">
<div className="w-full prose">
<h1 className="">CLI configuration</h1>
@@ -9,7 +9,15 @@ const sections = flattenSections(clientLibsCommonSections)
const libraryPath = '/dart/v0'
export default function JSReference(props) {
return <RefSectionHandler sections={sections} spec={spec} pageProps={props} type="client-lib" />
return (
<RefSectionHandler
sections={sections}
spec={spec}
pageProps={props}
type="client-lib"
isOldVersion
/>
)
}
export async function getStaticProps() {
@@ -1,4 +1,3 @@
import { inspect } from 'util'
import clientLibsCommonSections from '~/../../spec/common-client-libs-sections.json'
import typeSpec from '~/../../spec/enrichments/tsdoc_v1/combined.json'
import spec from '~/../../spec/supabase_js_v1.yml' assert { type: 'yml' }
@@ -18,6 +17,7 @@ export default function JSReference(props) {
typeSpec={typeSpec}
pageProps={props}
type="client-lib"
isOldVersion
/>
)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

+3 -3
View File
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.99982 10.0368H12.9998V14.0368H2.99982V10.0368Z" fill="#133929" stroke="#4CC38A" stroke-width="0.636673" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<rect x="3.50189" y="5.9998" width="8.99609" height="4" stroke="#4CC38A" stroke-width="0.636673" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M2.99982 2.00008H12.9998V6.00008H2.99982V2.00008Z" fill="#133929" stroke="#4CC38A" stroke-width="0.636673" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M2.99982 10.0368H12.9998V14.0368H2.99982V10.0368Z" fill="#133929" stroke="#4CC38A" stroke-width="1" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<rect x="3.50189" y="5.9998" width="8.99609" height="4" stroke="#4CC38A" stroke-width="1" stroke-miterlimit="10" stroke-linejoin="bevel"/>
<path d="M2.99982 2.00008H12.9998V6.00008H2.99982V2.00008Z" fill="#133929" stroke="#4CC38A" stroke-width="1" stroke-miterlimit="10" stroke-linejoin="bevel"/>
</svg>

Before

Width:  |  Height:  |  Size: 579 B

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

+6
View File
@@ -552,6 +552,12 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/integrations/cloudflare-workers</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/integrations/dhiwise</loc>
<changefreq>weekly</changefreq>
+28 -3
View File
@@ -96,6 +96,23 @@ export function splitTreeBy(tree: Root, predicate: (node: Content) => boolean) {
}, [])
}
/**
* Parses a markdown heading which can optionally
* contain a custom anchor in the format:
*
* ```markdown
* ### My Heading [#my-custom-anchor]
* ```
*/
export function parseHeading(heading: string): { heading: string; customAnchor?: string } {
const match = heading.match(/(.*) *\[#(.*)\]/)
if (match) {
const [, heading, customAnchor] = match
return { heading, customAnchor }
}
return { heading }
}
/**
* Processes MDX content for search indexing.
* It extracts metadata, strips it of all JSX,
@@ -140,12 +157,20 @@ export function processMdxForSearch(content: string): ProcessedMdx {
const sections = sectionTrees.map((tree) => {
const [firstNode] = tree.children
const content = toMarkdown(tree)
const heading = firstNode.type === 'heading' ? toString(firstNode) : undefined
const slug = heading ? slugger.slug(heading) : undefined
const rawHeading: string = firstNode.type === 'heading' ? toString(firstNode) : undefined
if (!rawHeading) {
return { content }
}
const { heading, customAnchor } = parseHeading(rawHeading)
const slug = slugger.slug(customAnchor ?? heading)
return {
content: toMarkdown(tree),
content,
heading,
slug,
}
+4
View File
@@ -80,6 +80,10 @@ article h1 {
@apply text-scale-900 text-xs;
}
.prose :where(p):not(:where([class~='not-prose'] *)) {
white-space: pre-line;
}
code[class*='language-'],
pre[class*='language-'] {
text-shadow: none !important;
@@ -40,7 +40,7 @@ We've made some massive improvements to our Table Editor that we're excited to s
Last month we made it easy to drill into your table relationships. This month, we make it possible to drill multiple levels deep.
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/relational-drilldown-zoom.mp4"
type="video/mp4"
@@ -51,7 +51,7 @@ Last month we made it easy to drill into your table relationships. This month, w
We're making it easier to manipulate your data. Next month, you'll be able to add and remove columns directly from the Table view.
<video width="99%" autoPlay="" loop="" muted="" playsInline="" controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/csv-download-zoom.mp4"
type="video/mp4"
+3 -12
View File
@@ -36,13 +36,10 @@ Supabase Auth provides all the backend services you need to authenticate and aut
Supabase makes it simple to onboard your users with our new `supabase.auth.signUp()` and `supabase.auth.signIn()` [functions](/docs/guides/auth).
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/auth-zoom2.mp4"
type="video/mp4"
loop
muted
playsInline
/>
</video>
@@ -50,13 +47,10 @@ Supabase makes it simple to onboard your users with our new `supabase.auth.signU
Authentication only gets you so far. When you need granular authorization rules, nothing beats PostgreSQL's [Row Level Security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html). Supabase makes it simple to turn RLS on and off.
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/rls-zoom2.mp4"
type="video/mp4"
loop
muted
playsInline
/>
</video>
@@ -64,13 +58,10 @@ Authentication only gets you so far. When you need granular authorization rules,
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are PostgreSQL's rule engine. They are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.
<video width="99%" autoPlay="autoplay" loop muted playsInline controls="true">
<video width="99%" autoPlay loop muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/policies-zoom2.mp4"
type="video/mp4"
loop
muted
playsInline
/>
</video>
Loaded 100 of 312 files, more files were not shown because too many files have changed in this diff. Show more