Merge branch 'master' into chore/update-blog-post
No files matched your search
@@ -20,7 +20,6 @@ export function SearchProvider({ children }: any) {
|
||||
const router = useRouter()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [initialQuery, setInitialQuery] = useState(null)
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
setIsOpen(true)
|
||||
}, [setIsOpen])
|
||||
@@ -85,6 +84,7 @@ export function SearchProvider({ children }: any) {
|
||||
hitComponent={Hit}
|
||||
transformItems={(items) => {
|
||||
return items.map((item, index) => {
|
||||
console.log('item', item)
|
||||
// We transform the absolute URL into a relative URL to
|
||||
// leverage Next's preloading.
|
||||
const a = document.createElement('a')
|
||||
@@ -96,7 +96,8 @@ export function SearchProvider({ children }: any) {
|
||||
...item,
|
||||
url: `${a.pathname}${hash}`,
|
||||
__is_result: () => true,
|
||||
__is_parent: () => item.type === 'lvl1' && items.length > 1 && index === 0,
|
||||
__is_parent: () => item.type === 'lvl1' && index === 0,
|
||||
__is_guide: () => item.hierarchy.lvl0 === 'Guides',
|
||||
__is_child: () =>
|
||||
item.type !== 'lvl1' &&
|
||||
items.length > 1 &&
|
||||
@@ -125,6 +126,7 @@ function Hit({ hit, children }) {
|
||||
'DocSearch-Hit--FirstChild': hit.__is_first?.(),
|
||||
'DocSearch-Hit--LastChild': hit.__is_last?.(),
|
||||
'DocSearch-Hit--Child': hit.__is_child?.(),
|
||||
'DocSearch-Hit--Guide': hit.__is_guide?.(),
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -298,6 +298,7 @@ export const auth = {
|
||||
url: '/guides/auth/social-login',
|
||||
items: [...SocialLoginItems],
|
||||
},
|
||||
{ name: 'Email Templates', url: '/guides/auth/auth-email-templates', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -468,6 +469,7 @@ export const platform = {
|
||||
items: [
|
||||
{ name: 'Compute Add-ons', url: '/guides/platform/compute-add-ons', items: [] },
|
||||
{ name: 'Custom Domains', url: '/guides/platform/custom-domains', items: [] },
|
||||
{ name: 'Database Backups', url: '/guides/platform/backups', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -674,6 +676,11 @@ export const integrations = {
|
||||
{ name: 'Plasmic', url: '/guides/integrations/plasmic', items: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Messaging',
|
||||
url: undefined,
|
||||
items: [{ name: 'OneSignal', url: '/guides/integrations/onesignal', items: [] }],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ import RefHeaderSection from './reference/RefHeaderSection'
|
||||
// Ref version specific
|
||||
import CliGlobalFlagsHandler from '~/components/reference/enrichments/cli/CliGlobalFlagsHandler'
|
||||
|
||||
import Options from '~/components/Options'
|
||||
import Param from '~/components/Params'
|
||||
|
||||
const components = {
|
||||
Admonition,
|
||||
Button,
|
||||
@@ -68,6 +71,8 @@ const components = {
|
||||
code: (props: any) => <CodeBlock {...props} />,
|
||||
RefHeaderSection: (props: any) => <RefHeaderSection {...props} />,
|
||||
CliGlobalFlagsHandler: () => <CliGlobalFlagsHandler />,
|
||||
Options,
|
||||
Param,
|
||||
}
|
||||
|
||||
export default components
|
||||
@@ -27,6 +27,7 @@ export type Flag = {
|
||||
description: string
|
||||
default_value: string
|
||||
accepted_values: AcceptedValue[]
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export type AcceptedValue = {
|
||||
@@ -107,7 +108,7 @@ const CliCommandSection = (props) => {
|
||||
{command.flags.map((flag: Flag) => (
|
||||
<>
|
||||
<li className="mt-0">
|
||||
<Param {...flag}>
|
||||
<Param {...flag} isOptional={!flag.required}>
|
||||
{flag?.accepted_values && (
|
||||
<Options>
|
||||
{flag?.accepted_values.map((value) => {
|
||||
|
||||
@@ -1,26 +1,8 @@
|
||||
const authProviders = [
|
||||
// {
|
||||
// name: 'Email',
|
||||
// // logo: '/img/libraries/dart-icon.svg',
|
||||
// href: '/docs/guides/auth/auth-apple',
|
||||
// official: true,
|
||||
// supporter: 'Supabase',
|
||||
// platform: true,
|
||||
// selfHosted: true,
|
||||
// },
|
||||
// {
|
||||
// name: 'Magic Links',
|
||||
// // logo: '/img/libraries/dart-icon.svg',
|
||||
// href: '/docs/guides/auth/auth-apple',
|
||||
// official: true,
|
||||
// supporter: 'Supabase',
|
||||
// platform: true,
|
||||
// selfHosted: true,
|
||||
// },
|
||||
{
|
||||
name: 'Apple',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-apple',
|
||||
href: '/guides/auth/social-login/auth-apple',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -29,7 +11,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Azure',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-azure',
|
||||
href: '/guides/auth/social-login/auth-azure',
|
||||
official: false,
|
||||
supporter: 'TBD',
|
||||
platform: true,
|
||||
@@ -38,7 +20,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Bitbucket',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-bitbucket',
|
||||
href: '/guides/auth/social-login/auth-bitbucket',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -47,7 +29,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Discord',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-discord',
|
||||
href: '/guides/auth/social-login/auth-discord',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -56,7 +38,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Facebook',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-facebook',
|
||||
href: '/guides/auth/social-login/auth-facebook',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -65,7 +47,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'GitHub',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-github',
|
||||
href: '/guides/auth/social-login/auth-github',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -74,7 +56,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'GitLab',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-gitlab',
|
||||
href: '/guides/auth/social-login/auth-gitlab',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -83,7 +65,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Google',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-google',
|
||||
href: '/guides/auth/social-login/auth-google',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -91,7 +73,7 @@ const authProviders = [
|
||||
},
|
||||
{
|
||||
name: 'Keycloak',
|
||||
href: '/guides/auth/auth-keycloak',
|
||||
href: '/guides/auth/social-login/auth-keycloak',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -100,7 +82,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'LinkedIn',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-linkedin',
|
||||
href: '/guides/auth/social-login/auth-linkedin',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -109,7 +91,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'MessageBird',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-messagebird',
|
||||
href: '/guides/auth/phone-login/messagebird',
|
||||
official: false,
|
||||
supporter: 'MessageBird',
|
||||
platform: true,
|
||||
@@ -118,7 +100,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Notion',
|
||||
// logo: '/img/libraries/notion-icon.svg',
|
||||
href: '/guides/auth/auth-notion',
|
||||
href: '/guides/auth/social-login/auth-notion',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -127,7 +109,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Slack',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-slack',
|
||||
href: '/guides/auth/social-login/auth-slack',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -136,7 +118,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Spotify',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-spotify',
|
||||
href: '/guides/auth/social-login/auth-spotify',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -145,7 +127,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Twitter',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-twitter',
|
||||
href: '/guides/auth/social-login/auth-twitter',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -154,7 +136,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Twitch',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-twitch',
|
||||
href: '/guides/auth/social-login/auth-twitch',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -163,7 +145,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Zoom',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-zoom',
|
||||
href: '/guides/auth/social-login/auth-zoom',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -172,7 +154,7 @@ const authProviders = [
|
||||
{
|
||||
name: 'Twilio',
|
||||
// logo: '/img/libraries/dart-icon.svg',
|
||||
href: '/guides/auth/auth-twilio',
|
||||
href: '/guides/auth/phone-login/twilio',
|
||||
official: true,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
@@ -180,7 +162,7 @@ const authProviders = [
|
||||
},
|
||||
{
|
||||
name: 'Vonage',
|
||||
href: '/guides/auth/auth-vonage',
|
||||
href: '/guides/auth/phone-login/vonage',
|
||||
official: false,
|
||||
supporter: 'Supabase',
|
||||
platform: true,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import sections from '../../../../spec/common-api-sections.json' assert { type: 'json' }
|
||||
import { flattenSections } from '../helpers.mjs'
|
||||
|
||||
const flatSections = flattenSections(sections)
|
||||
|
||||
export function generateAPIPages() {
|
||||
let apiPages = []
|
||||
|
||||
flatSections.map((section) => {
|
||||
apiPages.push(`reference/api/${section.slug}`)
|
||||
})
|
||||
return apiPages
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import fs from 'fs'
|
||||
import yaml from 'js-yaml'
|
||||
import cliCommonSections from '../../../../spec/common-cli-sections.json' assert { type: 'json' }
|
||||
import { flattenSections } from '../helpers.mjs'
|
||||
|
||||
const flatCLISections = flattenSections(cliCommonSections)
|
||||
|
||||
const cliSpec = yaml.load(fs.readFileSync(`../../spec/cli_v1_commands.yaml`, 'utf8'))
|
||||
|
||||
export function generateCLIPages() {
|
||||
let cliPages = []
|
||||
|
||||
cliSpec.commands.map((section) => {
|
||||
const slug = flatCLISections.find((item) => item.id === section.id)?.slug
|
||||
if (slug) cliPages.push(`reference/cli/${slug}`)
|
||||
})
|
||||
return cliPages
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import yaml from 'js-yaml'
|
||||
import commonLibSections from '../../../../spec/common-client-libs-sections.json' assert { type: 'json' }
|
||||
import { flattenSections } from '../helpers.mjs'
|
||||
|
||||
const flatCommonLibSections = flattenSections(commonLibSections)
|
||||
|
||||
const clientLibFiles = [
|
||||
{ fileName: 'supabase_js_v2', label: 'javascript', version: 'v2', versionSlug: false },
|
||||
{ fileName: 'supabase_js_v1', label: 'javascript', version: 'v1', versionSlug: true },
|
||||
{ fileName: 'supabase_dart_v1', label: 'dart', version: 'v1', versionSlug: false },
|
||||
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
|
||||
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
|
||||
]
|
||||
|
||||
export function generateReferencePages() {
|
||||
let refPages = []
|
||||
clientLibFiles.map((file) => {
|
||||
const spec = yaml.load(fs.readFileSync(`../../spec/${file.fileName}.yml`, 'utf8'))
|
||||
spec.functions.map((fn) => {
|
||||
const slug = flatCommonLibSections.find((item) => item.id === fn.id)?.slug
|
||||
refPages.push(`reference/${file.label}/${file.versionSlug ? file.version + '/' : ''}${slug}`)
|
||||
})
|
||||
})
|
||||
return refPages
|
||||
}
|
||||
@@ -1,65 +1,49 @@
|
||||
import { writeFileSync } from 'fs'
|
||||
import { globby } from 'globby'
|
||||
import prettier from 'prettier'
|
||||
|
||||
/*
|
||||
* kudos to leerob from vercel
|
||||
* https://leerob.io/blog/nextjs-sitemap-robots
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'fs'
|
||||
import { globby } from 'globby'
|
||||
import prettier from 'prettier'
|
||||
import { generateCLIPages } from './files/cli.mjs'
|
||||
import { generateReferencePages } from './files/reference-lib.mjs'
|
||||
import { generateAPIPages } from './files/api.mjs'
|
||||
|
||||
const referencePages = generateReferencePages()
|
||||
const cliPages = generateCLIPages()
|
||||
const apiPages = generateAPIPages()
|
||||
|
||||
async function generate() {
|
||||
const prettierConfig = await prettier.resolveConfig('./.prettierrc.js')
|
||||
|
||||
const rawPages = await globby([
|
||||
// guides
|
||||
'docs/*.mdx',
|
||||
'!docs/404.mdx',
|
||||
'pages/**/*.mdx',
|
||||
// reference
|
||||
'docs/reference/*.mdx',
|
||||
'docs/reference/javascript/*.mdx',
|
||||
'docs/reference/javascript/generated/*.mdx',
|
||||
'!docs/reference/javascript/v1.mdx', // ignore this
|
||||
'docs/reference/dart/*.mdx',
|
||||
'docs/reference/dart/generated/*.mdx',
|
||||
'!docs/reference/dart/v0.mdx', // ignore this
|
||||
'docs/reference/api/*.mdx',
|
||||
'docs/reference/api/generated/*.mdx',
|
||||
'docs/reference/cli/*.mdx',
|
||||
'docs/reference/cli/generated/*.mdx',
|
||||
// misc reference
|
||||
'docs/reference/postgres/*.mdx',
|
||||
'docs/reference/postgres/generated/*.mdx',
|
||||
'docs/reference/realtime/*.mdx',
|
||||
'docs/reference/realtime/generated/*.mdx',
|
||||
'docs/reference/storage/*.mdx',
|
||||
'docs/reference/storage/generated/*.mdx',
|
||||
'docs/reference/auth/*.mdx',
|
||||
'docs/reference/auth/generated/*.mdx',
|
||||
'!pages/404.mdx',
|
||||
'!pages/404.tsx',
|
||||
'!pages/ref-pages.mdx',
|
||||
])
|
||||
|
||||
const pages = rawPages.map((x) => {
|
||||
const guidePages = rawPages.map((x) => {
|
||||
let string = x
|
||||
string = string.replace('/generated', '')
|
||||
string = string.replace('pages/', '')
|
||||
string = string.replace('.mdx', '')
|
||||
return string
|
||||
})
|
||||
|
||||
// add static OSS page
|
||||
pages.unshift('docs/oss')
|
||||
// add static homepage
|
||||
pages.unshift('docs')
|
||||
// combine the guidePages with the ref pages
|
||||
const allPages = guidePages.concat(referencePages, cliPages, apiPages)
|
||||
|
||||
const sitemap = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${pages
|
||||
${allPages
|
||||
.map((path) => {
|
||||
// fix homepage
|
||||
path = path.replace('pages/index', '')
|
||||
return `
|
||||
<url>
|
||||
<loc>${`https://supabase.com/${path.replace('pages/', 'docs/')}`}</loc>
|
||||
<loc>${`https://supabase.com/docs/${path}`}</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -75,7 +59,9 @@ async function generate() {
|
||||
})
|
||||
|
||||
const sitemapFilePath = `public/sitemap.xml`
|
||||
console.log(`Total of ${pages.length} pages in sitemap, located at /apps/docs/${sitemapFilePath}`)
|
||||
console.log(
|
||||
`Total of ${allPages.length} pages in sitemap, located at /apps/docs/${sitemapFilePath}`
|
||||
)
|
||||
|
||||
// eslint-disable-next-line no-sync
|
||||
writeFileSync(sitemapFilePath, formatted)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export function flattenSections(sections) {
|
||||
var a = []
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
if (sections[i].id) {
|
||||
// only push a section that has an id
|
||||
// these are reserved for sidebar subtitles
|
||||
a.push(sections[i])
|
||||
}
|
||||
if (sections[i].items) {
|
||||
// if there are subitems, loop through
|
||||
a = a.concat(flattenSections(sections[i].items))
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// generator files need their own tsconfig.json because they don't like "module": "esnext" setting that nextjs requires in the main tsconfig
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"noImplicitAny": false,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TsDoc } from '~/generator/legacy/definitions'
|
||||
import { TsDoc } from '../../generator/legacy/definitions'
|
||||
|
||||
import { values, mapValues } from 'lodash'
|
||||
import { OpenAPIV3 } from 'openapi-types'
|
||||
@@ -60,6 +60,8 @@ function recurseThroughParams(paramDefinition: any) {
|
||||
children = dereferenced.children
|
||||
} else if (dereferenced.type?.declaration?.children) {
|
||||
children = dereferenced.type.declaration.children
|
||||
} else if (dereferenced.type?.type === 'query') {
|
||||
// skip: ignore types created from `typeof` for now, like `type Fetch = typeof fetch`
|
||||
} else if (dereferenced.type?.type === 'union') {
|
||||
// skip: we don't want to show unions as nested parameters
|
||||
} else if (Object.keys(dereferenced).length === 0) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"build:sitemap": "node ./internals/generate-sitemap.mjs",
|
||||
"postbuild": "node ./scripts/build-search.js && node ./internals/generate-sitemap.mjs",
|
||||
"postbuild": "ts-node ./scripts/build-search.ts && node ./internals/generate-sitemap.mjs",
|
||||
"generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-js:v1 gen:supabase-js:v2 gen:realtime",
|
||||
"gen:api": "npm-run-all gen:api:usage",
|
||||
"gen:api:usage": "ts-node ./generator/index.ts gen --type api --url https://api.supabase.com --input ../../spec/transforms/api_v0_openapi_deparsed.json --output ./docs/reference/api/generated/usage.mdx",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Email Templates',
|
||||
description: 'Learn how to configure the email templates on Supabase.',
|
||||
}
|
||||
|
||||
You can customize the email messages used for the authentication flows. You can edit the following email templates:
|
||||
|
||||
- Confirm signup
|
||||
- Invite user
|
||||
- Magic Link
|
||||
- Change Email Address
|
||||
- Reset Password
|
||||
|
||||
## Terminology
|
||||
|
||||
The templating system provides the following variables for use:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `{{ .ConfirmationURL }}` | Contains the confirmation URL. For example, a signup confirmation URL would look like: `https://project-ref.supabase.co/auth/v1/verify?token={{ .TokenHash }}&type=signup&redirect_to=https://example.com/path` . |
|
||||
| `{{ .Token }}` | Contains a 6-digit One-Time-Password (OTP) that can be used instead of the `{{. ConfirmationURL }}` . |
|
||||
| `{{ .TokenHash }}` | Contains a hashed version of the `{{ .Token }}`. This is useful for constructing your own email link in the email template. |
|
||||
| `{{ .SiteURL }}` | Contains your application's Site URL. This can be configured in your project's [authentication settings](https://app.supabase.com/project/_/auth/url-configuration). |
|
||||
|
||||
## Limitations
|
||||
|
||||
### Email Prefetching
|
||||
|
||||
Certain email providers may have spam detection or other security features that prefetch URL links from incoming emails.
|
||||
In this scenario, the `{{ .ConfirmationURL }}` sent will be consumed instantly which leads to a "Token has expired or is invalid" error.
|
||||
To guard against this:
|
||||
|
||||
- Use an email OTP instead by including `{{ .Token }}` in the email template.
|
||||
- Create your own custom email link to redirect the user to a page where they can click on a button to confirm the action.
|
||||
For example, you can include the following in your email template:
|
||||
|
||||
```html
|
||||
<a href="{{ .SiteURL }}/confirm-signup?confirmation_url={{ .ConfirmationURL }}"
|
||||
>Confirm your signup
|
||||
</a>
|
||||
```
|
||||
|
||||
The user should be brought to a page on your site where they can confirm the action by clicking a button.
|
||||
The button should contain the actual confirmation link which can be obtained from parsing the `confirmation_url={{ .ConfirmationURL }}` query parameter in the URL.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -95,7 +95,7 @@ Create a new file at `/utils/supabase-browser.ts` and populate with the followin
|
||||
|
||||
```ts
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { Database } from '../db_types'
|
||||
import { Database } from '../lib/database.types'
|
||||
|
||||
export default createBrowserSupabaseClient<Database>()
|
||||
```
|
||||
@@ -109,7 +109,7 @@ Create a new file at `/utils/supabase-server.ts` and populate with the following
|
||||
```ts title="/utils/supabase-server.ts"
|
||||
import { headers, cookies } from 'next/headers'
|
||||
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { Database } from '../db_types'
|
||||
import { Database } from '../lib/database.types'
|
||||
|
||||
export default () =>
|
||||
createServerComponentSupabaseClient<Database>({
|
||||
@@ -139,6 +139,8 @@ Middleware runs before every route declared in the `matcher` array. Since we don
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
Create a new file at `/pages/middleware.js` and populate with the following:
|
||||
|
||||
```jsx title="middleware.js"
|
||||
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
@@ -164,6 +166,8 @@ export const config = {
|
||||
|
||||
<TabPanel id="ts" label="TypeScript">
|
||||
|
||||
Create a new file at `/middleware.ts` (a sibling of /pages) and populate with the following:
|
||||
|
||||
```tsx title="middleware.ts"
|
||||
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { NextResponse } from 'next/server'
|
||||
@@ -254,7 +258,7 @@ export default async function RootLayout({ children }) {
|
||||
}
|
||||
```
|
||||
|
||||
We don't want Next.js to cache this `session` value, so we need to export a `revalidate` value of `0`.
|
||||
We don't want Next.js to cache this `session` value, so we need to export a `revalidate` value of `0` in the layout file.
|
||||
|
||||
```jsx
|
||||
export const revalidate = 0
|
||||
@@ -641,7 +645,7 @@ Create a new file at `/app/realtime/posts.tsx` and populate with the following:
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Database } from '../../db_types'
|
||||
import { Database } from '../lib/database.types'
|
||||
import supabase from '../../utils/supabase-browser'
|
||||
|
||||
type Post = Database['public']['Tables']['posts']['Row']
|
||||
|
||||
@@ -162,6 +162,7 @@ const supabaseClient = useSupabaseClient<Database>()
|
||||
|
||||
```tsx
|
||||
// Creating a new supabase server client object (e.g. in API route):
|
||||
import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type { Database } from 'types_db'
|
||||
|
||||
|
||||
@@ -36,11 +36,11 @@ It differs from the `http` extension in that it is asynchronous by default. This
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
-- Example: enable the "pg_net" extension
|
||||
create schema if not exists net;
|
||||
create extension pg_net with schema net;
|
||||
-- Example: enable the "pg_net" extension.
|
||||
create extension pg_net;
|
||||
-- Note: The extension creates its own schema/namespace named "net" to avoid naming conflicts.
|
||||
|
||||
-- Example: disable the "plv8" extension
|
||||
-- Example: disable the "pg_net" extension
|
||||
drop extension if exists pg_net;
|
||||
drop schema net;
|
||||
```
|
||||
|
||||
@@ -10,8 +10,8 @@ The `uuid-ossp` extension can be used to generate a `UUID`.
|
||||
|
||||
## Overview
|
||||
|
||||
A `UUID` is a "Universally Unique Identifer" and it is, for practical purposes, unique.
|
||||
This makes them particularly well suited as Primary Keys. It is occasionally referred to as a `GUID`, which stands for "Globally Unique Identifer".
|
||||
A `UUID` is a "Universally Unique Identifier" and it is, for practical purposes, unique.
|
||||
This makes them particularly well suited as Primary Keys. It is occasionally referred to as a `GUID`, which stands for "Globally Unique Identifier".
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ For example if you had the following situations:
|
||||
|
||||
- You have a list of `movies`.
|
||||
- A movie can have several `actors`.
|
||||
- An `actor` can perfom in several movies.
|
||||
- An `actor` can perform in several movies.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
|
||||
@@ -150,13 +150,13 @@ export const resources = [
|
||||
{
|
||||
title: 'Features',
|
||||
hasLightIcon: true,
|
||||
href: '/features',
|
||||
href: '/guides/getting-started/features',
|
||||
description: 'A non-exhaustive list of features that Supabase provides for every project.',
|
||||
},
|
||||
{
|
||||
title: 'Architecture',
|
||||
hasLightIcon: true,
|
||||
href: '/architecture',
|
||||
href: '/guides/getting-started/architecture',
|
||||
description: "An overview of Supabase's architecture and product principles.",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -60,11 +60,11 @@ Build passwordless logins for your application or website.[Docs](/docs/guides/au
|
||||
|
||||
### Social Logins
|
||||
|
||||
Provide social logins - everything from Apple, to GitHub, to Slack. [Docs](/docs/guides/auth/auth-apple).
|
||||
Provide social logins - everything from Apple, to GitHub, to Slack. [Docs](/docs/guides/auth/social-login).
|
||||
|
||||
### Phone Logins
|
||||
|
||||
Provide phone logins using a 3rd-party SMS provider. [Docs](/docs/guides/auth/auth-twilio).
|
||||
Provide phone logins using a third-party SMS provider. [Docs](/docs/guides/auth/phone-login).
|
||||
|
||||
### Row Level Security
|
||||
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'onesignal',
|
||||
title: 'OneSignal',
|
||||
description:
|
||||
'OneSignal allows you to send cloud messages to your users. Combine OneSignal with your Supabase apps and you can reach out to your users whenever there is a change in your database.',
|
||||
}
|
||||
|
||||
[OneSignal](https://onesignal.com/) is a tool that allows you to send messages across different channels such as the following to keep your users engaged.
|
||||
|
||||
- Push notifications
|
||||
- SMS
|
||||
- Emails
|
||||
- In-app notifications
|
||||
|
||||
This guide steps you through how you can integrate OneSignal with Supabase to create a seamless cloud messaging experience for your users using Database webhooks and edge functions through a simple Next.js application.
|
||||
|
||||

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

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

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

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

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

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

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

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

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

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

|
||||
|
||||
After locking in the desired point in time to recover to, The Dashboard will then prompt for a review and confirmation before proceeding with the restoration. The project will be inaccessible following this. As such, do ensure to allot for downtime beforehand. This is dependent on the size of the database. The larger it is, the longer the downtime will be. Once the confirmation has been given, the latest physical backup available is downloaded to the project and the database is partially restored. WAL files generated after this physical backup up to the specified point-in-time are then downloaded. The underlying records of transactions in these files are replayed against the database to complete the restoration. The Dashboard will display a notification once the restoration completes.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -12,18 +12,20 @@ Custom Domains are currently in beta, and are being slowly made available to pro
|
||||
|
||||
</Admonition>
|
||||
|
||||
Custom domains allow you to present a branded experience to your users. Currently, you must use a subdomain (e.g., `api.example.com`, rather than `example.com`) for the purposes of this guide.
|
||||
Custom domains allow you to present a branded experience to your users. Custom domains are available as a [add-on for projects on a paid tier](https://app.supabase.com/project/_/settings/billing/update). Setting up a custom domain requires [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. Currently, you must use a subdomain (e.g., `api.example.com`, rather than `example.com`) for the purposes of this guide.
|
||||
|
||||
## Configure a Custom Domain using the Supabase Dashboard
|
||||
|
||||
Follow the **Custom Domains** steps in the [General Settings](https://app.supabase.com/project/_/settings/general) page in the Dashboard to set up a custom domain for your project.
|
||||
|
||||
## Configure a Custom Domain using the Supabase CLI
|
||||
|
||||
This example assumes your Supabase project is `foobarbaz` with a corresponding API URL `foobarbaz.supabase.co` and configures a custom domain at `api.example.com`.
|
||||
|
||||
To get started:
|
||||
|
||||
1. [Install](/docs/guides/cli) the Supabase CLI 1.11.3+.
|
||||
1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform#manage-team-members) for the project you'd like to set up a custom domain for.
|
||||
1. Enable the Custom Domains add-on in the Billing page of the Dashboard.
|
||||
|
||||
## Configure a Custom Domain
|
||||
|
||||
This example assumes your Supabase project is `foobarbaz` with a corresponding API URL `foobarbaz.supabase.co` and configures a custom domain at `api.example.com`.
|
||||
1. [Install](/docs/guides/resources/supabase-cli) the Supabase CLI 1.11.3+.
|
||||
1. [Log in](/docs/guides/resources/supabase-cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
|
||||
### Configure a CNAME
|
||||
|
||||
@@ -47,7 +49,7 @@ Required outstanding validation records:
|
||||
api.example.com TXT -> ca3-F1HvR9i938OgVwpCFwi1jTsbhe1hvT0Ic3efPY3Q
|
||||
```
|
||||
|
||||
[^1] One of the records requires you to _replace_ the CNAME record set up in [the first step](#cname-configure) with a TXT record. You'll be able to restore it back to the CNAME after the verification process has been completed.
|
||||
[^1] One of the records requires you to _replace_ the CNAME record set up in [the first step](#configure-a-cname) with a TXT record. You'll be able to restore it back to the CNAME after the verification process has been completed.
|
||||
|
||||
### Verify your domain
|
||||
|
||||
@@ -97,9 +99,9 @@ Vanity Subdomains allow you to present a basic branded experience, compared to c
|
||||
|
||||
To get started:
|
||||
|
||||
1. [Install](/docs/guides/cli) the Supabase CLI 1.22.0+.
|
||||
1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/hosting/platform#manage-team-members) for the project you'd like to set up a vanity subdomain for.
|
||||
1. [Install](/docs/guides/resources/supabase-cli) the Supabase CLI 1.22.0+.
|
||||
1. [Log in](/docs/guides/resources/supabase-cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project you'd like to set up a vanity subdomain for.
|
||||
1. Ensure that your project has a paid subscription (Pro/Pay as you go/Enterprise tier) in the [Billing page of the Dashboard](https://app.supabase.com/project/_/settings/billing/subscription).
|
||||
|
||||
## Configure a Vanity Subdomain
|
||||
|
||||
@@ -54,10 +54,31 @@ After developing your project and deciding it's Production Ready, you should run
|
||||
- Supabase employs a number of safeguards against bursts of incoming traffic to prevent abuse and help maximize stability across the platform
|
||||
- If you're expecting high load events including production launches or heavy load testing, or prolonged high resource usage please give us at least 2 weeks notice. You can do this by opening a ticket via the [support form](https://app.supabase.com/support/new).
|
||||
|
||||
### Rate Limits
|
||||
|
||||
- The table below shows the rate limit quotas on the following authentication endpoints:
|
||||
|
||||
| Endpoint | Path | Limited By | Rate Limit |
|
||||
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------- |
|
||||
| All endpoints that send emails | `/auth/v1/signup` `/auth/v1/recover` `/auth/v1/user`[^1] | Sum of combined requests | Defaults to 30 emails per hour. Is customizable with custom SMTP set up. |
|
||||
| All endpoints that send One-Time-Passwords (OTP) | `/auth/v1/otp` | Sum of combined requests | Defaults to 30 OTPs per hour. Is customizable. |
|
||||
| Send OTPs or magiclinks | `/auth/v1/otp` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Signup confirmation request | `/auth/v1/signup` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Password Reset Request | `/auth/v1/recover` | Last request | Defaults to 60 seconds window before a new request is allowed. Is customizable. |
|
||||
| Verification requests | `/auth/v1/verify` | IP Address | 360 requests per hour (with bursts up to 30 requests) |
|
||||
| Token refresh requests | `/auth/v1/token` | IP Address | 360 requests per hour (with bursts up to 30 requests) |
|
||||
| Create or Verify an MFA challenge | `/auth/v1/factors/:id/challenge` `/auth/v1/factors/:id/verify` | IP Address | 15 requests per minute (with bursts up to 30 requests) |
|
||||
|
||||
### Abuse Prevention
|
||||
|
||||
- Supabase provides CAPTCHA protection on the signup, sign-in and password reset endpoints. Please refer to [our guide](/docs/guides/auth/auth-captcha) on how to protect against abuse using this method.
|
||||
|
||||
## Next steps
|
||||
|
||||
This checklist is always growing so be sure to check back frequently, and also feel free to suggest additions and amendments by making a PR on [GitHub](https://github.com/supabase/supabase).
|
||||
|
||||
[^1]: The rate limit is only applied on `/auth/v1/user` if this endpoint is called to update the user's email address.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -20,7 +20,7 @@ To get started:
|
||||
|
||||
1. [Install](/docs/guides/cli) the Supabase CLI 1.22.0+.
|
||||
1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/hosting/platform#manage-team-members) for the project that you are enabling network restrictions.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/hosting/platform/access-control#manage-team-members) for the project that you are enabling network restrictions.
|
||||
|
||||
## Check restrictions
|
||||
|
||||
|
||||
@@ -63,7 +63,14 @@ const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY
|
||||
*/
|
||||
const channel = supabase
|
||||
.channel('schema-db-changes')
|
||||
.on('postgres_changes', { event: '*', schema: 'public' }, (payload) => console.log(payload))
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
},
|
||||
(payload) => console.log(payload)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
@@ -74,8 +81,14 @@ To listen to changes on a table in the `public` schema:
|
||||
|
||||
const channel = supabase
|
||||
.channel('table-db-changes')
|
||||
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, (payload) =>
|
||||
console.log(payload)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'messages',
|
||||
},
|
||||
(payload) => console.log(payload)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
@@ -109,11 +122,21 @@ const channel = supabase
|
||||
.channel('db-changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: '*', schema: 'public', table: 'messages', filter: 'body=eq.bye' },
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'messages',
|
||||
filter: 'body=eq.bye',
|
||||
},
|
||||
(payload) => console.log(payload)
|
||||
)
|
||||
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'users' }, (payload) =>
|
||||
console.log(payload)
|
||||
.on('postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'users',
|
||||
},
|
||||
(payload) => console.log(payload)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
@@ -102,6 +102,10 @@ Once you have done this, you can safely comment out the `db` section of the `doc
|
||||
By default, `docker-compose` sets the database's `log_min_messages` configuration to `fatal` to prevent redundant logs generated by Realtime.
|
||||
However, you might miss important log messages such as database errors. Configure `log_min_messages` based on your needs.
|
||||
|
||||
### File storage backend on macOS
|
||||
|
||||
By default, Storage backend is set to `file`, which is to use local files as the storage backend. To make it work on macOS, you need to choose `VirtioFS` as the Docker container file sharing implementation (in Docker Desktop -> Preferences -> General).
|
||||
|
||||
## Deploying
|
||||
|
||||
See the following guides to deploy Docker Compose setup using your preferred tool and platform:
|
||||
|
||||
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
@@ -1,22 +1,24 @@
|
||||
const fs = require('fs')
|
||||
const crypto = require('crypto')
|
||||
const path = require('path')
|
||||
const matter = require('gray-matter')
|
||||
const dotenv = require('dotenv')
|
||||
const algoliasearch = require('algoliasearch/lite')
|
||||
const { isEmpty } = require('lodash')
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
import dotenv from 'dotenv'
|
||||
import algoliasearch from 'algoliasearch/lite'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
// [Joshen] We initially thought of building out our search using Algolia directly,
|
||||
// but eventually decided to just use DocSearch since it provides us a UI and some
|
||||
// configurations out of the box (e.g Doc hierarchy).
|
||||
// search objects
|
||||
import { generateClientLibSearchObjects } from './files/client-libs'
|
||||
import { generateAPISearchObjects } from './files/api'
|
||||
import { generateCLISearchObjects } from './files/cli'
|
||||
|
||||
// As such when creating the search object, the properties that are specifically read
|
||||
const cliObjects = generateCLISearchObjects()
|
||||
const apiObjects = generateAPISearchObjects()
|
||||
const clientLibSearchObjects = generateClientLibSearchObjects()
|
||||
|
||||
// @ts-ignore
|
||||
// The properties of the searchObject that are specifically read
|
||||
// by DocSearch are "type" and "hierarchy". The rest, though saved into Algolia (which we
|
||||
// can potentially use to craft more nuanced search experiences) are not used by DocSearch
|
||||
|
||||
// Note that we'll need to do a general clean up of the files in the docs
|
||||
// A lot of them are not even linked to within the docs site, so just need to
|
||||
// double check if they can be removed or if we want them in the side bars.
|
||||
const ignoredFiles = [
|
||||
'pages/404.mdx',
|
||||
'pages/faq.mdx',
|
||||
@@ -30,18 +32,9 @@ const ignoredFiles = [
|
||||
'pages/handbook/supasquad.mdx',
|
||||
]
|
||||
|
||||
const nameMap = {
|
||||
api: 'Management API',
|
||||
cli: 'Supabase CLI',
|
||||
auth: 'Auth Server',
|
||||
storage: 'Storage Server',
|
||||
postgres: 'Postgres',
|
||||
dart: 'Supabase Flutter Library',
|
||||
javascript: 'Supabase JavaScript Library',
|
||||
}
|
||||
|
||||
async function walk(dir) {
|
||||
let files = await fs.promises.readdir(dir)
|
||||
//@ts-ignore
|
||||
files = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const filePath = path.join(dir, file)
|
||||
@@ -66,15 +59,6 @@ async function walk(dir) {
|
||||
|
||||
console.log('Preparing docs indexing for Algolia')
|
||||
|
||||
function extractMeta(mdx) {
|
||||
const regex = /^export\sconst\smeta\s=\s(\{[\w\s':,]*\})/gm
|
||||
const match = regex.exec(mdx)
|
||||
if (match) {
|
||||
return JSON.parse(match[1])
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const indexName = process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME
|
||||
const client = algoliasearch(
|
||||
@@ -83,11 +67,10 @@ async function walk(dir) {
|
||||
)
|
||||
const index = client.initIndex(indexName)
|
||||
|
||||
const referencePages = await walk('docs')
|
||||
const guidePages = (await walk('pages')).filter((slug) => !ignoredFiles.includes(slug))
|
||||
const allPages = guidePages.concat(referencePages)
|
||||
|
||||
const searchObjects = allPages
|
||||
// generate search objects for mdx guide pages
|
||||
const guidePagesearchObjects = guidePages
|
||||
.map((slug) => {
|
||||
let id, title, description
|
||||
const fileContents = fs.readFileSync(slug, 'utf8')
|
||||
@@ -118,6 +101,7 @@ async function walk(dir) {
|
||||
.replace('pages', '')
|
||||
.replace(/\.mdx$/, '')
|
||||
const source = slug.includes('/reference') ? 'reference' : 'guide'
|
||||
|
||||
const object = {
|
||||
// For Algolia
|
||||
objectID: crypto.randomUUID(),
|
||||
@@ -126,7 +110,8 @@ async function walk(dir) {
|
||||
description,
|
||||
url,
|
||||
source,
|
||||
pageContent: content,
|
||||
//pageContent: content,
|
||||
pageContent: '',
|
||||
category: undefined,
|
||||
version: undefined,
|
||||
|
||||
@@ -143,44 +128,26 @@ async function walk(dir) {
|
||||
},
|
||||
}
|
||||
|
||||
if (slug.includes('/reference')) {
|
||||
const urlSegments = url.split('/')
|
||||
const category = urlSegments[urlSegments.indexOf('reference') + 1]
|
||||
const version = urlSegments[urlSegments.indexOf('reference') + 2] || ''
|
||||
|
||||
const hasVersion = /v[0-9]+/.test(version)
|
||||
if (hasVersion) {
|
||||
object.version = version
|
||||
object.type = title === nameMap[category] ? 'lvl2' : 'lvl3'
|
||||
object.hierarchy.lvl1 = nameMap[category]
|
||||
object.hierarchy.lvl2 = version
|
||||
object.hierarchy.lvl3 = title
|
||||
} else {
|
||||
const page = urlSegments[urlSegments.indexOf('reference') + 2]
|
||||
if (page === 'generated') {
|
||||
} else {
|
||||
if (page !== undefined) {
|
||||
object.type = 'lvl2'
|
||||
object.hierarchy.lvl1 = nameMap[category]
|
||||
object.hierarchy.lvl2 = title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object.category = category ? category.replace(/\.mdx$/, '') : undefined
|
||||
object.hierarchy.lvl0 = 'References'
|
||||
}
|
||||
|
||||
return object
|
||||
})
|
||||
// Some of the reference generated files come with an 'index' page that we can ignore
|
||||
.filter((object) => !object.url.endsWith('/index'))
|
||||
.filter((object) => !object.url.endsWith('/.gitkeep'))
|
||||
|
||||
const combinedSearchObjects = guidePagesearchObjects.concat(
|
||||
clientLibSearchObjects,
|
||||
apiObjects,
|
||||
cliObjects
|
||||
)
|
||||
|
||||
//@ts-ignore
|
||||
await index.clearObjects()
|
||||
console.log(`Successfully cleared records from ${indexName}`)
|
||||
|
||||
const algoliaResponse = await index.saveObjects(searchObjects)
|
||||
//@ts-ignore
|
||||
const algoliaResponse = await index.saveObjects(combinedSearchObjects)
|
||||
|
||||
//@ts-ignore
|
||||
console.log(`Successfully saved ${algoliaResponse.objectIDs.length} records into ${indexName}.`)
|
||||
} catch (error) {
|
||||
console.log('Error:', error)
|
||||
@@ -0,0 +1,43 @@
|
||||
import crypto from 'crypto'
|
||||
import { flattenSections } from '../../lib/helpers'
|
||||
|
||||
import apiCommonSections from '~/../../spec/common-api-sections.json'
|
||||
import specFile from '~/../../spec/transforms/api_v0_openapi_deparsed.json'
|
||||
import { gen_v3 } from '../../lib/refGenerator/helpers'
|
||||
|
||||
// @ts-ignore
|
||||
const generatedSpec = gen_v3(specFile, 'wat', { apiUrl: 'apiv0' })
|
||||
const sections = flattenSections(apiCommonSections)
|
||||
|
||||
export function generateAPISearchObjects() {
|
||||
let searchObjects = []
|
||||
|
||||
//@ts-ignore
|
||||
sections.map((section) => {
|
||||
const object = searchObjects.push({
|
||||
objectID: crypto.randomUUID(),
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
// @ts-ignore
|
||||
description: generatedSpec.operations.find((item) => item.operationId === section.id)
|
||||
?.summary,
|
||||
url: `/reference/api/${section.slug}`,
|
||||
source: 'reference',
|
||||
pageContent: '',
|
||||
category: section.product,
|
||||
version: '',
|
||||
type: 'lvl2',
|
||||
hierarchy: {
|
||||
lvl0: 'References',
|
||||
lvl1: 'Management API',
|
||||
lvl2: section.title,
|
||||
lvl3: null,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
})
|
||||
return object
|
||||
})
|
||||
return searchObjects
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs'
|
||||
import { flattenSections } from '../../lib/helpers'
|
||||
import cliCommonSections from '~/../../spec/common-cli-sections.json'
|
||||
import yaml from 'js-yaml'
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
const spec = yaml.load(fs.readFileSync(`../../spec/cli_v1_commands.yaml`, 'utf8'))
|
||||
|
||||
const commonSections = flattenSections(cliCommonSections)
|
||||
|
||||
export function generateCLISearchObjects() {
|
||||
let searchObjects = []
|
||||
|
||||
//@ts-ignore
|
||||
spec.commands.map((section) => {
|
||||
const object = searchObjects.push({
|
||||
objectID: crypto.randomUUID(),
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
// @ts-ignore
|
||||
description: section.description?.substr(0, section.description.indexOf('\n')),
|
||||
url: `/reference/cli/${commonSections.find((item) => item.id === section.id)?.slug}`,
|
||||
source: 'reference',
|
||||
pageContent: '',
|
||||
category: section.product,
|
||||
version: '',
|
||||
type: 'lvl2',
|
||||
hierarchy: {
|
||||
lvl0: 'References',
|
||||
lvl1: 'Supabase CLI',
|
||||
lvl2: section.title,
|
||||
lvl3: null,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
})
|
||||
return object
|
||||
})
|
||||
return searchObjects
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto'
|
||||
import yaml from 'js-yaml'
|
||||
import { flattenSections } from '../../lib/helpers'
|
||||
import { nameMap } from '../helpers'
|
||||
|
||||
import commonLibSections from '~/../../spec/common-client-libs-sections.json'
|
||||
|
||||
const clientLibFiles = [
|
||||
{ fileName: 'supabase_js_v2', label: 'javascript', version: 'v2', versionSlug: false },
|
||||
{ fileName: 'supabase_js_v1', label: 'javascript', version: 'v1', versionSlug: true },
|
||||
{ fileName: 'supabase_dart_v1', label: 'dart', version: 'v1', versionSlug: false },
|
||||
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
|
||||
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
|
||||
]
|
||||
|
||||
const flatCommonLibSections = flattenSections(commonLibSections)
|
||||
|
||||
export function generateClientLibSearchObjects() {
|
||||
// loop through each spec file, find the correspending entry in the common file and grab the title / description / slug
|
||||
let clientLibSearchObjects = []
|
||||
|
||||
clientLibFiles.map((file) => {
|
||||
const specs = yaml.load(fs.readFileSync(`../../spec/${file.fileName}.yml`, 'utf8'))
|
||||
|
||||
//take each function id, find it in the commonLibSections file and return { id, title, slug, description, }
|
||||
//@ts-ignore
|
||||
specs.functions.map((fn) => {
|
||||
const item = flatCommonLibSections.find((section) => section.id === fn.id)
|
||||
if (item) {
|
||||
const object = clientLibSearchObjects.push({
|
||||
objectID: crypto.randomUUID(),
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
description: item.title,
|
||||
url: `/reference/${file.label}/${file.versionSlug ? file.version + '/' : ''}${item.slug}`,
|
||||
source: 'reference',
|
||||
pageContent: '',
|
||||
category: item.product,
|
||||
version: file.version,
|
||||
type: 'lvl2',
|
||||
hierarchy: {
|
||||
lvl0: 'References',
|
||||
lvl1: `${nameMap[file.label]} ${file.version}`,
|
||||
lvl2: item.title,
|
||||
lvl3: file.version,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
})
|
||||
return object
|
||||
}
|
||||
})
|
||||
})
|
||||
return clientLibSearchObjects
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export const nameMap = {
|
||||
api: 'Management API',
|
||||
cli: 'Supabase CLI',
|
||||
auth: 'Auth Server',
|
||||
storage: 'Storage Server',
|
||||
postgres: 'Postgres',
|
||||
dart: 'Supabase Flutter Library',
|
||||
javascript: 'Supabase JavaScript Library',
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// generator files need their own tsconfig.json because they don't like "module": "esnext" setting that nextjs requires in the main tsconfig
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"noImplicitAny": false,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -81,8 +81,7 @@
|
||||
--aa-overlay-color-alpha: 0.4;
|
||||
|
||||
// Shadows
|
||||
--aa-panel-shadow: 0 0 0 1px rgba(35, 38, 59, 0.1),
|
||||
0 6px 16px -4px rgba(35, 38, 59, 0.15);
|
||||
--aa-panel-shadow: 0 0 0 1px rgba(35, 38, 59, 0.1), 0 6px 16px -4px rgba(35, 38, 59, 0.15);
|
||||
|
||||
// Scrollbar
|
||||
--aa-scrollbar-width: 13px;
|
||||
@@ -122,8 +121,7 @@ body {
|
||||
--aa-icon-color-rgb: 119, 119, 163;
|
||||
|
||||
// Shadows
|
||||
--aa-panel-shadow: inset 1px 1px 0 0 rgb(44, 46, 64),
|
||||
0 3px 8px 0 rgb(0, 3, 9);
|
||||
--aa-panel-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9);
|
||||
|
||||
// Scrollbar
|
||||
--aa-scrollbar-track-background-color-rgb: 44, 46, 64;
|
||||
@@ -429,10 +427,7 @@ body {
|
||||
}
|
||||
// Title typography
|
||||
@at-root .aa-SourceHeaderTitle {
|
||||
background: rgba(
|
||||
var(--aa-background-color-rgb),
|
||||
var(--aa-background-color-alpha)
|
||||
);
|
||||
background: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
|
||||
color: rgba(var(--aa-primary-color-rgb), 1);
|
||||
display: inline-block;
|
||||
font-size: 0.8em;
|
||||
@@ -464,8 +459,7 @@ body {
|
||||
rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha)),
|
||||
rgba(128, 126, 163, 0.14)
|
||||
);
|
||||
border: 1px solid
|
||||
rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
|
||||
border: 1px solid rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
|
||||
border-radius: 5px;
|
||||
box-shadow: inset 0 0 2px #fff, 0 2px 2px -1px rgba(76, 69, 88, 0.15);
|
||||
color: inherit;
|
||||
@@ -502,10 +496,7 @@ body {
|
||||
// The result type icon inlined SVG or image
|
||||
@at-root .aa-ItemIcon {
|
||||
align-items: center;
|
||||
background: rgba(
|
||||
var(--aa-background-color-rgb),
|
||||
var(--aa-background-color-alpha)
|
||||
);
|
||||
background: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 0 1px
|
||||
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
|
||||
@@ -658,10 +649,7 @@ body {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@at-root .aa-ItemContentTag {
|
||||
background-color: rgba(
|
||||
var(--aa-primary-color-rgb),
|
||||
var(--aa-primary-color-alpha)
|
||||
);
|
||||
background-color: rgba(var(--aa-primary-color-rgb), var(--aa-primary-color-alpha));
|
||||
border-radius: 3px;
|
||||
margin: 0 0.4em 0 0;
|
||||
padding: 0.08em 0.3em;
|
||||
@@ -754,10 +742,7 @@ body {
|
||||
// 8. Panel Footer
|
||||
//----------------
|
||||
.aa-PanelFooter {
|
||||
background-color: rgba(
|
||||
var(--aa-background-color-rgb),
|
||||
var(--aa-background-color-alpha)
|
||||
);
|
||||
background-color: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
|
||||
box-shadow: inset 0 1px 0
|
||||
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
|
||||
display: flex;
|
||||
@@ -847,10 +832,7 @@ body {
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 1px
|
||||
rgba(
|
||||
var(--aa-panel-border-color-rgb),
|
||||
var(--aa-panel-border-color-alpha)
|
||||
);
|
||||
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -969,7 +951,11 @@ body {
|
||||
}
|
||||
|
||||
// search bar
|
||||
input[type='search'].DocSearch-Input {
|
||||
background-color: var(--colors-scale1);
|
||||
}
|
||||
|
||||
.dark .DocSearch-Input {
|
||||
background-color: var(--colors-scale2);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Cancel {
|
||||
button[type='reset'].DocSearch-Cancel {
|
||||
appearance: none;
|
||||
flex: none;
|
||||
font-size: 0;
|
||||
@@ -202,7 +202,7 @@
|
||||
@apply ring-scale-300 bg-white shadow-sm ring-1 dark:bg-slate-700 dark:ring-0;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--Parent .DocSearch-Hit-icon {
|
||||
.DocSearch-Hit--Parent.DocSearch-Hit--Guide .DocSearch-Hit-icon {
|
||||
/* book icon */
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='14' height='10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M3.5 0c1.093 0 2.117.27 3 .743V10a6.344 6.344 0 0 0-3-.743c-1.093 0-2.617.27-3.5.743V.743C.883.27 2.407 0 3.5 0Z' fill='%23BAE6FD'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.5 0c1.093 0 2.617.27 3.5.743V10c-.883-.473-2.407-.743-3.5-.743s-2.117.27-3 .743V.743a6.344 6.344 0 0 1 3-.743Z' fill='%2338BDF8'/%3E%3C/svg%3E");
|
||||
}
|
||||
@@ -289,7 +289,11 @@
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-action path {
|
||||
stroke: white;
|
||||
stroke: #777;
|
||||
|
||||
.dark & {
|
||||
stroke: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Hit > a {
|
||||
@@ -392,7 +396,7 @@
|
||||
|
||||
.DocSearch-StartScreen {
|
||||
.DocSearch-Help {
|
||||
@apply text-center py-16;
|
||||
@apply text-center py-16;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,4 +531,4 @@
|
||||
|
||||
.DocSearch-Hit-action + .DocSearch-Hit-action {
|
||||
// @apply ml-3 border-l border-slate-200 pl-3 dark:border-slate-200;
|
||||
}
|
||||
}
|
||||
@@ -66,4 +66,4 @@ We also have guides and tools for [migrating Firebase Auth to Supabase Auth](htt
|
||||
|
||||
These are by far the most complete Firebase to Postgres migration tools available anywhere on the web.
|
||||
|
||||
You can [try Supabase for free](https://app.supabase.com). If you require Enterprise level support with your project or migration, please get in touch using our [Enterprise contact form](https://supabase.com/contact/enterprise).
|
||||
You can [try Supabase for free](https://app.supabase.com). If you require Enterprise level support with your project or migration, please get in touch using our [Enterprise contact form](https://forms.supabase.com/enterprise).
|
||||
@@ -57,13 +57,13 @@ Ever wanted to build a Search Engine? We just released a guide which shows you h
|
||||
### OAuth Guides
|
||||
|
||||
We released step-by-step guides to help you set up OAuth with
|
||||
[Apple](/docs/guides/auth/auth-apple),
|
||||
[Bitbucket](/docs/guides/auth/auth-bitbucket),
|
||||
[Facebook](/docs/guides/auth/auth-facebook),
|
||||
[GitHub](/docs/guides/auth/auth-github),
|
||||
[GitLab](/docs/guides/auth/auth-gitlab),
|
||||
[Google](/docs/guides/auth/auth-google), and
|
||||
[Twitter](/docs/guides/auth/auth-twitter).
|
||||
[Apple](/docs/guides/auth/social-login/auth-apple),
|
||||
[Bitbucket](/docs/guides/auth/social-login/auth-bitbucket),
|
||||
[Facebook](/docs/guides/auth/social-login/auth-facebook),
|
||||
[GitHub](/docs/guides/auth/social-login/auth-github),
|
||||
[GitLab](/docs/guides/auth/social-login/auth-gitlab),
|
||||
[Google](/docs/guides/auth/social-login/auth-google), and
|
||||
[Twitter](/docs/guides/auth/social-login/auth-twitter).
|
||||
|
||||

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

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

|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ We updated our [grid](https://github.com/supabase/grid) to support relationships
|
||||
|
||||
## Improved Auth Docs
|
||||
|
||||
We've revamped the Auth docs - The docs are now broken down into [Authentication](/docs/guides/auth/auth-apple) and [Authorization](/docs/guides/auth/row-level-security), and organized alongside our [Deep Dive](/docs/learn/auth-deep-dive/auth-deep-dive-jwts) series.
|
||||
We've revamped the Auth docs - The docs are now broken down into [Authentication](/docs/guides/auth/social-login/auth-apple) and [Authorization](/docs/guides/auth/row-level-security), and organized alongside our [Deep Dive](/docs/learn/auth-deep-dive/auth-deep-dive-jwts) series.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ Watch a recap of this month's release.
|
||||
|
||||
Thanks to [`@HarryET`](https://github.com/supabase/gotrue/pull/245) and our friends at [MessageBird](https://github.com/supabase/gotrue/pull/210)
|
||||
we have 3 new Auth providers this month:
|
||||
[Slack](https://supabase.com/docs/guides/auth/auth-slack),
|
||||
[Spotify](https://supabase.com/docs/guides/auth/auth-spotify), and
|
||||
[MessageBird](https://supabase.com/docs/guides/auth/auth-messagebird) phone logins.
|
||||
[Slack](https://supabase.com/docs/guides/auth/social-login/auth-slack),
|
||||
[Spotify](https://supabase.com/docs/guides/auth/social-login/auth-spotify), and
|
||||
[MessageBird](https://supabase.com/docs/guides/auth/phone-login/messagebird) phone logins.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ GitGuardian helps developers keep 250+ types of secrets out of source code. Thei
|
||||
|
||||

|
||||
|
||||
The beauty of building in the open is that the community can get involved in adding new OAuth providers, which is exactly what [@TheHarryET](https://twitter.com/TheHarryET) and [MonsterDeveloper](https://github.com/MonsterDeveloper) have been doing. Thanks to them you can now provide sign-in with [Slack](/docs/guides/auth/auth-slack) as well as [Spotify](/docs/guides/auth/auth-spotify) to your users, as well as use [MessageBird](/docs/guides/auth/auth-messagebird) for phone auth as an alternative to the existing [Twilio](/docs/guides/auth/auth-twilio) integration.
|
||||
The beauty of building in the open is that the community can get involved in adding new OAuth providers, which is exactly what [@TheHarryET](https://twitter.com/TheHarryET) and [MonsterDeveloper](https://github.com/MonsterDeveloper) have been doing. Thanks to them you can now provide sign-in with [Slack](/docs/guides/auth/social-login/auth-slack) as well as [Spotify](/docs/guides/auth/social-login/auth-spotify) to your users, as well as use [MessageBird](/docs/guides/auth/phone-login/messagebird) for phone auth as an alternative to the existing [Twilio](/docs/guides/auth/phone-login/twilio) integration.
|
||||
|
||||
## SupaSquad updates
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ Also added by [devkiran](https://github.com/devkiran).
|
||||
|
||||
### Other SMS providers
|
||||
|
||||
Just a reminder, we also support [Twilio](https://supabase.com/docs/guides/auth/auth-twilio) and [MessageBird](https://supabase.com/docs/guides/auth/auth-messagebird).
|
||||
Just a reminder, we also support [Twilio](https://supabase.com/docs/guides/auth/phone-login/twilio) and [MessageBird](https://supabase.com/docs/guides/auth/phone-login/messagebird).
|
||||
|
||||
On a final note, we are hiring for an [Auth Engineer](https://about.supabase.com/careers/auth-engineers).
|
||||
|
||||
|
||||
@@ -112,9 +112,9 @@ When finished, PostgREST v10 will also include the ability to `limit` updates an
|
||||
|
||||
The beauty of open source is that anyone can contribute to make the project even better. This is especially true for supabase-auth as we get contributions and ideas for new oauth providers every month! This launch week, we’ve had 4 new OAuth provider contributions:
|
||||
|
||||
- [Keycloak](https://supabase.com/docs/guides/auth/auth-keycloak) ([@fspijkerman](https://github.com/fspijkerman))
|
||||
- [Notion](https://supabase.com/docs/guides/auth/auth-notion) ([@zernonia](https://github.com/zernonia))
|
||||
- [Zoom](https://supabase.com/docs/guides/auth/auth-zoom) ([@devkiran](https://github.com/devkiran))
|
||||
- [Keycloak](https://supabase.com/docs/guides/auth/social-login/auth-keycloak) ([@fspijkerman](https://github.com/fspijkerman))
|
||||
- [Notion](https://supabase.com/docs/guides/auth/social-login/auth-notion) ([@zernonia](https://github.com/zernonia))
|
||||
- [Zoom](https://supabase.com/docs/guides/auth/social-login/auth-zoom) ([@devkiran](https://github.com/devkiran))
|
||||
- [WorkOS](https://supabase.com/docs/guides/auth/auth-workos) ([@bnjmnt4n](https://github.com/bnjmnt4n))
|
||||
|
||||
### New Phone providers
|
||||
@@ -123,7 +123,7 @@ The beauty of open source is that anyone can contribute to make the project even
|
||||
|
||||
We’ve also added 2 new phone providers from the same contributor:
|
||||
|
||||
- [Vonage](https://supabase.com/docs/guides/auth/auth-vonage) ([@devkiran](https://github.com/devkiran))
|
||||
- [Vonage](https://supabase.com/docs/guides/auth/phone-login/vonage) ([@devkiran](https://github.com/devkiran))
|
||||
- [TextLocal](https://github.com/supabase/gotrue/pull/342) ([@devkiran](https://github.com/devkiran))
|
||||
|
||||
If you haven’t tried out phone auth yet, check out [this blog post](https://supabase.com/blog/supabase-auth-passwordless-sms-login) to get started!
|
||||
|
||||
@@ -70,7 +70,7 @@ We've released granular permissions for the Supabase Dashboard. Dashboard permis
|
||||
We're starting with 3 roles—Owner, Administrator, and Developer—which you can edit in the Org settings of the Dashboard.
|
||||
There's a tooltip in the header of the members table, which elaborates on the individual permissions available for each role.
|
||||
|
||||
[Read more](/docs/guides/platform#manage-team-members).
|
||||
[Read more](/docs/guides/platform/access-control#manage-team-members).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,47 +1,49 @@
|
||||
---
|
||||
title: 'pg_crdt - an experimental CRDT extension for Postgres'
|
||||
description: "Embedding Yjs and Automerge into Postgres for collaborative applications."
|
||||
description: 'Embedding Yjs and Automerge into Postgres for collaborative applications.'
|
||||
author: paul_copplestone
|
||||
image: crdt/crdt-blog.png
|
||||
thumb: crdt/crdt-blog.png
|
||||
image: launch-week-6/crdt/og-pg-crdt.png
|
||||
thumb: launch-week-6/crdt/og-pg-crdt.png
|
||||
tags:
|
||||
- launch-week
|
||||
date: '2022-12-10'
|
||||
toc_depth: 2
|
||||
---
|
||||
|
||||
Today we’re open-sourcing an EXPERIMENTAL extension for CRDTs, `pg_crdt`. The GitHub repo is [here](https://github.com/supabase/pg_crdt). There are [instructions](https://github.com/supabase/pg_crdt#installation) for running it locally in the README.
|
||||
Today we're open-sourcing an EXPERIMENTAL extension for CRDTs, `pg_crdt`. The GitHub repo is [here](https://github.com/supabase/pg_crdt). There are [instructions](https://github.com/supabase/pg_crdt#installation) for running it locally in the README.
|
||||
|
||||
When we released the new [multiplayer features](/blog/supabase-realtime-multiplayer-general-availability) for our Realtime engine,
|
||||
it took 30 minutes for someone to ask if we’d add CRDT support.
|
||||
When we released the new [multiplayer features](/blog/supabase-realtime-multiplayer-general-availability) for our Realtime engine,
|
||||
it took 30 minutes for someone to ask if we'd add CRDT support.
|
||||
|
||||
> *Anyone from Supabase here, do you have any plans to build in support for CRDT toolkits such as Yjs or AutoMerge for these features? It would make working with them so much easier if there was a plug and play backend.*
|
||||
> _Anyone from Supabase here, do you have any plans to build in support for CRDT toolkits such as Yjs or AutoMerge for these features? It would make working with them so much easier if there was a plug and play backend._
|
||||
|
||||
<small><i>@samwillis on [HackerNews](https://news.ycombinator.com/item?id=32510820)</i></small>
|
||||
<small>
|
||||
<i>@samwillis on [HackerNews](https://news.ycombinator.com/item?id=32510820)</i>
|
||||
</small>
|
||||
|
||||
pg_crdt has not been released onto the Supabase platform (and it may never be). We’re considering many options for offline-sync/support and, while CRDTs will undoubtedly factor in, we’re not sure if this is the *right* approach. Hopefully this release generates a healthy discussion about the various ways we can do it at Supabase.
|
||||
pg_crdt has not been released onto the Supabase platform (and it may never be). We're considering many options for offline-sync/support and, while CRDTs will undoubtedly factor in, we're not sure if this is the right approach. Hopefully this release generates a healthy discussion about the various ways we can do it at Supabase.
|
||||
|
||||
## What’s a CRDT?
|
||||
## What's a CRDT?
|
||||
|
||||
A CRDT (Conflict-free Replicated Data Type) is a data structure. More accurately, a family of data structures. They enable collaborative apps like [Figma](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/).
|
||||
|
||||
You already know what a “data structure” is: an Array is a good example. CRDTs are a *special* type of data structure designed to solve a specific problem: they can merge changes in a way that the final state of the data will be the same, no matter the order in which the updates were applied.
|
||||
You already know what a “data structure” is: an Array is a good example. CRDTs are a _special_ type of data structure designed to solve a specific problem: they can merge changes in a way that the final state of the data will be the same, no matter the order in which the updates were applied.
|
||||
|
||||
In simple terms, a CRDT allows multiple users to make changes to the same data without the need for a central authority to coordinate their actions.
|
||||
|
||||
Let’s use our Array example to demonstrate the problem they solve. Imagine you have an array (which is not a CRDT):
|
||||
Let's use our Array example to demonstrate the problem they solve. Imagine you have an array (which is not a CRDT):
|
||||
|
||||
```jsx
|
||||
let fruit = ['Apple', 'Banana', 'Orange']
|
||||
```
|
||||
|
||||
Now imagine you have 2 developers updating this array on their local computers. They both want to replace the fruit at the start of the array. The first user, let’s call her “Jenny”, does this:
|
||||
Now imagine you have 2 developers updating this array on their local computers. They both want to replace the fruit at the start of the array. The first user, let's call her “Jenny”, does this:
|
||||
|
||||
```js
|
||||
fruit[0] = 'Grape' // the array is ['Grape', 'Banana', 'Orange']
|
||||
```
|
||||
|
||||
The developer sitting next to her, let’s call him “Jonny”, does the same:
|
||||
The developer sitting next to her, let's call him “Jonny”, does the same:
|
||||
|
||||
```js
|
||||
fruit[0] = 'Mango' // the array is ['Mango', 'Banana', 'Orange']
|
||||
@@ -51,36 +53,36 @@ And now, since they are both developing on different machines, they push their c
|
||||
|
||||

|
||||
|
||||
Since this is one of those *basic* arrays, the answer is "the fruit array that arrives last”.
|
||||
Since this is one of those _basic_ arrays, the answer is "the fruit array that arrives last”.
|
||||
|
||||
1. If Jonny’s fruit array arrives first, it will be saved.
|
||||
2. After that, Jenny’s fruit arrived and overwrites Jonny’s changes.
|
||||
1. If Jonny's fruit array arrives first, it will be saved.
|
||||
2. After that, Jenny's fruit arrived and overwrites Jonny's changes.
|
||||
|
||||
But therein lies the problem: Jonny was the last developer to change his array of fruit, shouldn’t his changes be the ones that are saved?
|
||||
But therein lies the problem: Jonny was the last developer to change his array of fruit, shouldn't his changes be the ones that are saved?
|
||||
|
||||
That’s one of the things that CRDTs solve. There is an “array CRDT” which, when merged, will always have the same result. It doesn’t matter if Jonny’s array arrives first, or if Jenny’s arrives first - every time you merge them they would be able to determine that the Jonny’s was the last change the fruit array.
|
||||
That's one of the things that CRDTs solve. There is an “array CRDT” which, when merged, will always have the same result. It doesn't matter if Jonny's array arrives first, or if Jenny's arrives first - every time you merge them they would be able to determine that the Jonny's was the last change the fruit array.
|
||||
|
||||
How does it do that? Some sort of algorithm, but you can ask ChatGPT to explain that one.
|
||||
|
||||
## Offline philosophy
|
||||
|
||||
Collaborative apps are becoming more prolific as legacy software is rebuilt within a browser environment.
|
||||
Collaborative apps are becoming more prolific as legacy software is rebuilt within a browser environment.
|
||||
|
||||
Collaboration is largely a data problem - how do we get one user’s changes (data) to merge with another user’s changes (data)? As a database provider it’s natural fit for Supabase to provide the tooling for developers to build collaborative apps.
|
||||
Collaboration is largely a data problem - how do we get one user's changes (data) to merge with another user's changes (data)? As a database provider it's natural fit for Supabase to provide the tooling for developers to build collaborative apps.
|
||||
|
||||
Before going too far down the “solution” rabbit hole at Supabase, we try to think about the long-term implications of adopting any technology. We’re pretty boring - we don’t make bets on technologies unless we think they will exist in 20 years.
|
||||
Before going too far down the “solution” rabbit hole at Supabase, we try to think about the long-term implications of adopting any technology. We're pretty boring - we don't make bets on technologies unless we think they will exist in 20 years.
|
||||
|
||||
I personally believe CRDTs are the future. For *some things*. If databases were invented today, I’m certain most of the effort would be spent developing CRDT databases or something with “offline algorithms” built-in. But tech is a real-world demonstration of the Lindy effect: the longer something has existed, the longer it’s likely to exist in the future. That’s why we bet on Postgres - with more than 30 years history, it’s here to stay.
|
||||
I personally believe CRDTs are the future. For _some things_. If databases were invented today, I'm certain most of the effort would be spent developing CRDT databases or something with “offline algorithms” built-in. But tech is a real-world demonstration of the Lindy effect: the longer something has existed, the longer it's likely to exist in the future. That's why we bet on Postgres - with more than 30 years history, it's here to stay.
|
||||
|
||||
Faced with this reality, we should consider how to solve *offline with Postgres* and where CRDTs might fit into that picture.
|
||||
Faced with this reality, we should consider how to solve _offline with Postgres_ and where CRDTs might fit into that picture.
|
||||
|
||||
For a long time I thought there could be a way to shoehorn Postgres row-level data into a CRDT, to give *truly* offline support. This may even still be [possible](https://electric-sql.com/), and it’s one of the ideas we’ll continue to pursue. But Postgres is a rich and evolving ecosystem, and I don’t know whether it will be possible to -
|
||||
For a long time I thought there could be a way to shoehorn Postgres row-level data into a CRDT, to give _truly_ offline support. This may even still be [possible](https://electric-sql.com/), and it's one of the ideas we'll continue to pursue. But Postgres is a rich and evolving ecosystem, and I don't know whether it will be possible to -
|
||||
|
||||
a) find a merge strategy for an entire row, while simultaneously:
|
||||
|
||||
b) finding a merge strategy for every data type *within* that row (especially with the number of data types available through extensions)
|
||||
b) finding a merge strategy for every data type _within_ that row (especially with the number of data types available through extensions)
|
||||
|
||||
It’s possible that developers will need to be selective about their “level” of offline support, at least if they plan to use an “incumbent” databases. For the cases, a simple “last write wins” strategy is probably acceptable (see the excellent [Replicache](https://doc.replicache.dev/examples/repliear) and [Watermelon](https://nozbe.github.io/WatermelonDB/) libraries), but there will be important pieces of their application where it is not.
|
||||
It's possible that developers will need to be selective about their “level” of offline support, at least if they plan to use an “incumbent” databases. For the cases, a simple “last write wins” strategy is probably acceptable (see the excellent [Replicache](https://doc.replicache.dev/examples/repliear) and [Watermelon](https://nozbe.github.io/WatermelonDB/) libraries), but there will be important pieces of their application where it is not.
|
||||
|
||||
Take this table of blog posts as an example:
|
||||
|
||||
@@ -92,9 +94,9 @@ create table posts (
|
||||
);
|
||||
```
|
||||
|
||||
Perhaps it’s not that important if the `title` has a “last write wins” strategy, because it’s rarely updated and less likely to have a merge conflict. But it is critical to use a smarter algorithm for the `content` of the blog post, especially in a team where multiple users are editing a blog post at the same time.
|
||||
Perhaps it's not that important if the `title` has a “last write wins” strategy, because it's rarely updated and less likely to have a merge conflict. But it is critical to use a smarter algorithm for the `content` of the blog post, especially in a team where multiple users are editing a blog post at the same time.
|
||||
|
||||
That’s a lot of background to get to what you probably want to know about:
|
||||
That's a lot of background to get to what you probably want to know about:
|
||||
|
||||
## Postgres CRDT extension
|
||||
|
||||
@@ -108,7 +110,7 @@ create table posts (
|
||||
);
|
||||
```
|
||||
|
||||
The `content` column is now a [Yjs Doc](https://docs.yjs.dev/api/y.doc). Updates to this column are *commutative* and *idempotent*. This means that they can be applied in any order and multiple times, and the result will always be the same. Two people can edit the blog content at the same time, and they won’t have any issues when their changes are saved in the database.
|
||||
The `content` column is now a [Yjs Doc](https://docs.yjs.dev/api/y.doc). Updates to this column are _commutative_ and _idempotent_. This means that they can be applied in any order and multiple times, and the result will always be the same. Two people can edit the blog content at the same time, and they won't have any issues when their changes are saved in the database.
|
||||
|
||||
### Support for Yjs, Automerge, and beyond
|
||||
|
||||
@@ -116,7 +118,7 @@ The Yjs and Automerge teams have done some excellent work to create Rust librari
|
||||
|
||||
At this stage it makes sense to give developers as many choices as possible in one extension. The CRDT space is nascent the algorithms are rapidly changing.
|
||||
|
||||
Importantly, both Yjs and Automerge have *JavaScript* and Rust implementations, which means they work natively in both a browser and a Postgres environment. Since collaborative applications are largely a client-side problem, CRDTs are more useful for developers if they have robust JavaScript libraries. In the future, if this extension becomes the approach we take, then Supabase will focus some resources on building Yjs/Automerge libraries for mobile devices too (Swift for iOS, Kotlin for Android).
|
||||
Importantly, both Yjs and Automerge have _JavaScript_ and Rust implementations, which means they work natively in both a browser and a Postgres environment. Since collaborative applications are largely a client-side problem, CRDTs are more useful for developers if they have robust JavaScript libraries. In the future, if this extension becomes the approach we take, then Supabase will focus some resources on building Yjs/Automerge libraries for mobile devices too (Swift for iOS, Kotlin for Android).
|
||||
|
||||
## Why not build this into Realtime?
|
||||
|
||||
@@ -128,18 +130,18 @@ This might still be our best option, but it should not be our first attempt. If
|
||||
|
||||

|
||||
|
||||
The Realtime engine seems like a great *compliment* for `pg_crdt`, but before we put it into production we need to solve a few (major) limitations.
|
||||
The Realtime engine seems like a great _compliment_ for `pg_crdt`, but before we put it into production we need to solve a few (major) limitations.
|
||||
|
||||
## Limitations
|
||||
|
||||
These are a few of the *known* limitations:
|
||||
These are a few of the _known_ limitations:
|
||||
|
||||
- Realtime broadcasts database changes from the Postgres write ahead log (WAL). The WAL includes a complete copy of the the underlying data so small updates cause the entire document to broadcast to all collaborators
|
||||
- Frequently updated CRDTs produce a lot of WAL and dead tuples
|
||||
- Large CRDT types in Postgres generate significant serialization/deserialization overhead on-update.
|
||||
|
||||
We’re likely to discover more (no doubt from a few friendly HN comments).
|
||||
We're likely to discover more (no doubt from a few friendly HN comments).
|
||||
|
||||
## Next steps
|
||||
|
||||
If you want to help with `pg_crdt` the best way is to get involved in the GitHub repo. We have enabled [Discussions](https://github.com/supabase/pg_crdt/discussions) for any and all ideas. If you have experience with CRDTs and you like this approach, don’t hesitate to contact one of the team.
|
||||
If you want to help with `pg_crdt` the best way is to get involved in the GitHub repo. We have enabled [Discussions](https://github.com/supabase/pg_crdt/discussions) for any and all ideas. If you have experience with CRDTs and you like this approach, don't hesitate to contact one of the team.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 'New Supabase Docs, built with Next.js'
|
||||
description: We've redesignd our Docs and migrated to Next.js
|
||||
description: We've redesigned our Docs and migrated to Next.js
|
||||
author: jonny
|
||||
image: lw6-day-1/lw6-day1-docs.jpg
|
||||
thumb: lw6-day-1/lw6-day1-docs.jpg
|
||||
|
||||
@@ -14,7 +14,10 @@ toc_depth: 3
|
||||
---
|
||||
Today, we’re releasing Multi Factor Authentication for everyone.
|
||||
|
||||
Additionally, in preparation for releasing SAML, we're "dogfooding" the feature with the introduction of Single Sign On (SSO) on our dashboard. Contact us at [growth@supabase.com](mailto:growth@supabase.com) if you want to [enable this in your Enterprise or Pro plan](https://docs.supabase.com/guides/platform/sso/enable-sso).
|
||||
Additionally, in preparation for releasing SAML, we're "dogfooding" the feature with the introduction of Single Sign On (SSO) on our dashboard. Contact us at [growth@supabase.com](mailto:growth@supabase.com) if you want to [enable this on your Enterprise plan](https://supabase.com/docs/guides/platform/sso).
|
||||
|
||||
<iframe
|
||||
width="560" height="315" src="https://www.youtube-nocookie.com/embed/He7LI2mv9v0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||
## What is MFA?
|
||||
|
||||
@@ -124,3 +127,11 @@ Be mindful of your user’s preference, though. If a user has enabled MFA, they
|
||||
For starters, we are looking to support WebAuthn and FIDO2 compliant devices such as Yubikeys. We also hope to allow users to receive email notifications when selected MFA actions are triggered. If you have MFA requirements which are not covered here feel free to write to us at support[at]supabase.io .
|
||||
|
||||
We are grateful to our early MFA users for the support and feedback provided throughout this period. In particular, we would like to thank [Fabian Beer](https://madebyfabian.com), [Cogram](https://www.cogram.com), and [Happl](https://happl.com) whose detailed feedback helped to shape our implementation. We would also like to specially thank the community behind the [pquerna/otp](https://github.com/pquerna/otp) and [ajstarks/svgo](https://github.com/ajstarks/svgo) libraries - their work is indispensable to this implementation.
|
||||
|
||||
## More Launch Week 6
|
||||
|
||||
- [Day 1: New Supabase Docs, built with Next.js](https://supabase.com/blog/new-supabase-docs-built-with-nextjs)
|
||||
- [Day 2: Supabase Storage v2: Image resizing and Smart CDN](https://supabase.com/blog/storage-image-resizing-smart-cdn)
|
||||
- [Launch Week 6 Hackathon](https://supabase.com/blog/launch-week-6-hackathon)
|
||||
- [Who We Hire at Supabase](https://supabase.com/blog/who-we-hire)
|
||||
- [pg_crdt - an experimental CRDT extension for Postgres](https://supabase.com/blog/postgres-crdt)
|
||||
@@ -0,0 +1,264 @@
|
||||
---
|
||||
title: 'Supabase Wrappers, a Postgres FDW framework written in Rust'
|
||||
description: 'A framework for building Postgres Foreign Data Wrappers which connects to Stripe, Firebase, Clickhouse, and more.'
|
||||
author: paul_copplestone,bo_lu,oli_rice
|
||||
image: launch-week-6/wrappers/wrappers-og.png
|
||||
thumb: launch-week-6/wrappers/wrappers-og.png
|
||||
tags:
|
||||
- launch-week
|
||||
date: '2022-12-15'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
Today we're releasing [Supabase Wrappers](https://github.com/supabase/wrappers), a framework for building Postgres Foreign Data Wrappers (FDW) which connects Postgres to external systems.
|
||||
|
||||
Foreign Data Wrappers are a core feature of PostgreSQL. We've extended this feature to query other databases or any other external system (including third-party APIs), using SQL.
|
||||
|
||||

|
||||
|
||||
We're releasing Wrappers today in Alpha, with support for [Firebase](https://supabase.github.io/wrappers/firebase/) and [Stripe](https://supabase.github.io/wrappers/stripe/). Wrappers for Clickhouse, BigQuery, and Airtable are under development.
|
||||
|
||||
## Example with Stripe
|
||||
|
||||
Let's step through a full example using the Stripe Wrapper.
|
||||
|
||||

|
||||
|
||||
### Connecting to Stripe
|
||||
|
||||
First, let's give your Postgres database some authentication details to access your Stripe account:
|
||||
|
||||
```sql
|
||||
create server stripe_server
|
||||
foreign data wrapper stripe_wrapper
|
||||
options (api_key 'sk_test_xxx');
|
||||
```
|
||||
|
||||
### Creating a Foreign Table
|
||||
|
||||
Now we can map your Stripe data to Foreign Tables, which are just like normal tables except that the data exists _outside_ of your database.
|
||||
|
||||
```sql
|
||||
-- Create a foreign table for your Stripe products
|
||||
create foreign table products (
|
||||
id text,
|
||||
name text,
|
||||
description text,
|
||||
default_price text
|
||||
)
|
||||
server my_stripe_server
|
||||
options ( object 'products' );
|
||||
```
|
||||
|
||||
### Accessing remote data
|
||||
|
||||
After setting up your foreign table, you can query you Stripe products directly from your database:
|
||||
|
||||
```sql
|
||||
-- Fetch all your stripe products in Postgres
|
||||
select *
|
||||
from products
|
||||
limit 10;
|
||||
```
|
||||
|
||||

|
||||
|
||||
Or from your application, using one of our client libraries:
|
||||
|
||||
```jsx
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const SUPABASE_URL = 'https://xyzcompany.supabase.co'
|
||||
const SUPABASE_KEY = 'public-anon-key'
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
|
||||
const { data: stripeCustomers, error } = supabase
|
||||
.from('products')
|
||||
.select('id, name, description, default_price')
|
||||
.limit(10)
|
||||
```
|
||||
|
||||
_Note: we kept this example simple, however for API security and code organization, you should store your foreign data in a [separate schema](/docs/reference/javascript/initializing#api-schemas)._
|
||||
|
||||
## Use cases
|
||||
|
||||
Once we've added more Wrappers, they enable various possibilities:
|
||||
|
||||
- **On-demand Big Data:** Query your Data Warehouse on demand using Wrappers for Clickhouse, BigQuery, Snowflake, Oracle, and S3.
|
||||
- **Simplified Onboarding:** Migrate to Postgres from systems like Firebase, MySQL, and Airtable.
|
||||
- **Simplified Development:** Create a new Stripe customer object within a Postgres function.
|
||||
- **AI capabilities:** Run AI queries from your database using OpenAI's API.
|
||||
- **Caching:** use Postgres triggers to insert data into in-memory databases like Redis.
|
||||
- **SRE and DevOps:** Query your infrastructure state, like AWS and DNS records.
|
||||
- **Web3 Apps:** integrate with IPFS and blockchains like Ethereum.
|
||||
- **Financial Apps:** Build Financial applications using wrappers around Finance APIs.
|
||||
- **Community analytics:** Analyze your community engagement with GitHub, Slack, Discord, and Twitter wrappers.
|
||||
|
||||
## On-demand ETL
|
||||
|
||||
In their 2017 [paper](https://www.sciencedirect.com/science/article/abs/pii/S0169023X1730438X), researchers from the University of Bologna investigated an approach to on-demand ETL:
|
||||
|
||||
> In traditional OLAP systems, the ETL process loads all available data in the data warehouse before users start querying them. In some cases, this may be either inconvenient (because data are supplied from a provider for a fee) or unfeasible (because of their size); on the other hand, directly launching each analysis query on source data would not enable data reuse, leading to poor performance and high costs. The alternative investigated in this paper is that of fetching and storing data on-demand.
|
||||
|
||||
Their paper outlines the foundation for QETL (pronounced "kettle"): Query, Extract, Transform, Load. This differs from a more traditional ETL/ELT approach, where data is moved from one place to another. In QETL, the “Query” function allows data engineers to access data in a remote system even before moving it. This approach reduces the reliance on data engineering infrastructure, allowing teams to access operational insights faster.
|
||||
|
||||
### The benefits of QETL
|
||||
|
||||
We've built upon this concept using PostgreSQL's FDW system. This a new tool for developers and data engineers, with several benefits:
|
||||
|
||||
1. **Simplicity:** the Wrappers API is just SQL, so data engineers don't need to learn new tools and languages.
|
||||
2. **On-demand:** analytical data is available within your product without any additional infrastructure, and the time it takes to retrieve that data is close to executing a query on the source.
|
||||
3. **Always in sync:** data which isn't moved will always be in sync. Developers can set up local views which re-map remote data into clean local schemas.
|
||||
4. **Integrated:** large datasets are available within your application, and can be joined with your operational/transactional data.
|
||||
5. **Save on bandwidth:** only extract/load what you need.
|
||||
6. **Save on time:** avoid setting up additional data pipelines.
|
||||
7. **Save on Data Engineering costs:** less infrastructure to be managed.
|
||||
|
||||
### QETL + Postgres
|
||||
|
||||
How does this look in action? Assuming that all of your analytical data is stored in Snowflake, you could create a foreign table inside your Supabase database:
|
||||
|
||||
```sql
|
||||
create foreign table snowflake.order_history (
|
||||
id bigint,
|
||||
ts timestamptz,
|
||||
event text,
|
||||
user_id uuid
|
||||
)
|
||||
server my_snowflake_warehouse
|
||||
options (table 'order_history', rowid_column 'id');
|
||||
```
|
||||
|
||||
Now from your Supabase database you can query your Snowflake data directly:
|
||||
|
||||
```sql
|
||||
select * from snowflake.order_history limit 1000;
|
||||
```
|
||||
|
||||
You can even join remote data with your local tables to enrich existing operational data. For example, to figure out how many times a user has purchased something from your store:
|
||||
|
||||
```sql
|
||||
select
|
||||
users.id,
|
||||
count(order_history.event)
|
||||
from
|
||||
snowflake.order_history
|
||||
join
|
||||
auth.users on auth.users.id = snowflake.order_history.user_id
|
||||
where
|
||||
order_history.event = 'purchase' and
|
||||
order_history.user_id = '<some_user_id>';
|
||||
```
|
||||
|
||||
We can either run these queries on demand or, for better query performance, we can run them in the background (using something like [pg_cron](https://supabase.com/docs/guides/database/extensions/pgcron)), and materialize the data into a local table.
|
||||
|
||||

|
||||
|
||||
This gives us the basis of QETL:
|
||||
|
||||
- Query: run on-demand SQL queries, directly from your Postgres database.
|
||||
- Extract: run SQL `select` statements on external systems, either on demand or on a recurring basis.
|
||||
- Transform: use SQL aggregations and CTEs to transform the data.
|
||||
- Load: store transformed data into local tables for faster access.
|
||||
|
||||
This is a two-way process. It's equally useful to offload large datasets from your Postgres database to your Data Warehouse. With FDWs, this can be as simple as:
|
||||
|
||||
```sql
|
||||
insert into snowflake.analytics
|
||||
select * from analytics
|
||||
where ts > (now() - interval '1 DAY');
|
||||
```
|
||||
|
||||
On-demand ETL is a strong compliment for current ETL practices, and another tool in the toolbelt for Data Engineering and Developers that works with immediately with tools that interface with Postgres.
|
||||
|
||||
## Postgres, the everything interface
|
||||
|
||||
In a recent Software Engineering [episode](https://podcasts.apple.com/us/podcast/software-engineering-daily/id1019576853?i=1000580384354) Andy Pavlo (database Professor at Carnegie Mellon and Co-Founder of OtterTune), explored the future between “better databases” and “better interfaces” [00:37:18]:
|
||||
|
||||
<Quote img="andy-pavlo.jpeg" caption="Andy Pavlo">
|
||||
|
||||
Specialized engines are always going to outperform general-purpose ones. The question is whether
|
||||
the specialized engine is going to have such a significant difference in performance that it can
|
||||
overcome the limitations of a general purpose one.
|
||||
|
||||
...
|
||||
|
||||
The challenge oftentimes is this: is the benefit you're getting from a specialized engine
|
||||
because the _Engine_ is different or the _API_ is different?.
|
||||
|
||||
</Quote>
|
||||
|
||||
He goes on to explore the benefits of a Graph database vs a Relational database.
|
||||
|
||||
Our recent release of [pg_graphql](https://github.com/supabase/pg_graphql/) closes the gap on the graph use-case by building a GraphQL API directly into Postgres as an extension. While a specialized graph database might provide performance benefits over Postgres, perhaps one of the largest benefits is simply the Graph API which makes it easier to reason about the data.
|
||||
|
||||
With the introduction of Wrappers, we're hoping to close the gap on even more of these type of workloads.
|
||||
|
||||
An exciting part of the FDW approach is that it provides a common interface to the world: SQL. While it has many shortcomings, SQL is the lingua franca of data. Postgres' FDWs transform any API into a data set with a common interface. This “interface aggregator” is similar to the [promise](https://medium.com/vicetech/graphql-the-great-aggregator-bcf52fe390d9) of GraphQL engines. The benefit of embedding this functionality in the database is that it exists at the _lowest level of the tech stack_. Everything that is built on top can leverage it. While Postgres cannot easily access the functionality of a GraphQL server, a GraphQL server can easily access the functionality of Postgres.
|
||||
|
||||
The FDW interface also future-proofs Postgres. Instead of keeping up with the latest technological advances, Postgres can instead act as an interface whenever they develop. The recent advance in AI and ML is a great example of this: AI technology is developing faster than the time it would take to build a new “AI database”. With a FDW, Postgres can become the interface to this technology and many other technological advances in the future.
|
||||
|
||||
## Developing Wrappers
|
||||
|
||||
Postgres has a builtin “Postgres FDW” allows querying one Postgres database from another. We've extended this functionality to support a variety of data sources, from Data Warehouses to APIs.
|
||||
This release includes two initial wrappers: Stripe and Firebase
|
||||
|
||||
| Integration | Platform | Self-hosted | <span style={{fontWeight: "normal"}}>select</span> | <span style={{fontWeight: "normal"}}>insert</span> | <span style={{fontWeight: "normal"}}>update</span> | <span style={{fontWeight: "normal"}}>delete</span> |
|
||||
| ----------- | -------- | ----------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
|
||||
| Firebase | 🚧 | ✅ | ✅ | 🚧 | 🚧 | 🚧 |
|
||||
| Postgres | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Stripe | 🚧 | ✅ | ✅ | 🚧 | 🚧 | 🚧 |
|
||||
|
||||
With several more [under development](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw):
|
||||
|
||||
| Integration | <span style={{fontWeight: "normal"}}>select</span> | <span style={{fontWeight: "normal"}}>insert</span> | <span style={{fontWeight: "normal"}}>update</span> | <span style={{fontWeight: "normal"}}>delete</span> |
|
||||
| ----------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- |
|
||||
| Airtable | ✅ | 🚧 | 🚧 | 🚧 |
|
||||
| BigQuery | ✅ | ✅ | ✅ | ✅ |
|
||||
| ClickHouse | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
Wrappers used [pgx](https://github.com/tcdi/pgx), extending it with FDW support. pgx is a Postgres extension framework written in Rust. Wrappers is very similar to [Steampipe](https://steampipe.io/) or [Multicorn](https://multicorn.org/). We opted to develop our own framework for several reasons:
|
||||
|
||||
- The current state of FDWs is in disarray. It's hard to know which FDWs are supported and functional. We think there's a benefit to colocating all FDWs in a single repository using modern tooling. This makes contributing simpler and maintenance faster.
|
||||
- Wrappers has async support, which enables the development of RESTful API-based FDWs, like Stripe.
|
||||
- It's written in Rust, which provides reliable performance, strong typing, data security, and “push down” is supported through the framework.
|
||||
|
||||
## Supported types and Push Down
|
||||
|
||||
Wrappers [supports](https://github.com/supabase/wrappers/blob/83887dcc2ddcf972ca1b1eec4a598cd7bff947de/supabase-wrappers/src/interface.rs#L11) a variety of types, including:
|
||||
`bool`, `i8`, `i16`, `f32`, `i32`, `f64`, `i64`, `String`, `Date`, `Timestamp`, and `JsonB`.
|
||||
|
||||
Foreign Data Wrappers have a concept of "push down". This means that the FDW runs the query on _remote_ data source. This is useful for performance reasons, as the remote data source can often perform the query more efficiently than Postgres. Push down is also useful for security reasons, as the remote data source can enforce access control. Limited push-down support has been added as a Proof of Concept, but this will be a key focus of Wrappers.
|
||||
|
||||
You can follow development of all the Wrappers in the official [GitHub Repo](https://github.com/supabase/wrappers).
|
||||
|
||||
## Next Steps
|
||||
|
||||
We're not "officially" releasing Wrappers onto the platform yet, although the brave and curious might be able to figure out how to use it "unofficially". Caveat emptor: there will be breaking changes.
|
||||
|
||||
We're excited to see what the community does with Wrappers. We're hoping that Wrappers will help to accelerate the adoption of Postgres as a data hub.
|
||||
If you're interested in getting involved or building your own Wrapper, don't hesitate to jump into the code and start developing with us.
|
||||
|
||||
- Star & Watch the GitHub repo: [github.com/supabase/wrappers](https://github.com/supabase/wrappers)
|
||||
- View the documentation: [supabase.github.io/wrappers](https://supabase.github.io/wrappers/)
|
||||
- Supabase Launch Week: [supabase.com/launch-week](https://supabase.com/launch-week)
|
||||
|
||||
<iframe
|
||||
className="video-with-border w-full"
|
||||
width="640"
|
||||
height="385"
|
||||
src="https://www.youtube-nocookie.com/embed/QA2qC5F-4OU"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
|
||||
## More Launch Week 6
|
||||
|
||||
- [Day 1: New Supabase Docs, built with Next.js](https://supabase.com/blog/new-supabase-docs-built-with-nextjs)
|
||||
- [Day 2: Supabase Storage v2: Image resizing and Smart CDN](https://supabase.com/blog/storage-image-resizing-smart-cdn)
|
||||
- [Day 3: Multi-factor Authentication via Row Level Security Enforcement](https://supabase.com/blog/mfa-auth-via-rls)
|
||||
- [Launch Week 6 Hackathon](https://supabase.com/blog/launch-week-6-hackathon)
|
||||
- [Who We Hire at Supabase](https://supabase.com/blog/who-we-hire)
|
||||
- [pg_crdt - an experimental CRDT extension for Postgres](https://supabase.com/blog/postgres-crdt)
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Custom Domain Names
|
||||
description: Change your Supabase project's domain name to your own domain.
|
||||
author: div_arora
|
||||
image: launch-week-6/custom-domains/og-custom-domains.png
|
||||
thumb: launch-week-6/custom-domains/og-custom-domains.png
|
||||
tags:
|
||||
- launch-week
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
After your side-project becomes wildly viral, and you've graduated into a “serious business”, delivering a more branded experience is probably one of the items at the bottom of your checklist.
|
||||
|
||||
To that end, we're releasing custom domains, and vanity subdomains, to help you present a more polished product to your users.
|
||||
|
||||
# Custom Domains
|
||||
|
||||
Custom domains allow you to use your own domain for your Supabase project.
|
||||
|
||||
Instead of seeing:
|
||||
|
||||
`sqvfdnkeiuztnsinmtau.supabase.co`
|
||||
|
||||
your users instead interact with
|
||||
|
||||
`api.example.com`:
|
||||
|
||||

|
||||
|
||||
Today, we're making Custom Domains generally available, and they're now accessible as a paid add-on through the Supabase dashboard.
|
||||
|
||||
Get your custom domain in the [Dashboard](https://app.supabase.com/project/_/settings/general)
|
||||
|
||||
# Vanity Subdomains
|
||||
|
||||
Vanity Subdomains allow you to choose a subdomain on Supabase's own domain.
|
||||
|
||||
Instead of hosting your services on a randomly assigned subdomain:
|
||||
|
||||
`abcdefghijklmno.supabase.co`
|
||||
|
||||
you can choose one that's relevant to your project, for example:
|
||||
|
||||
`my-example-brand.supabase.co`
|
||||
|
||||
Vanity Subdomains are offered at no additional cost for all projects on a paid plan, and are being launched in a closed beta.
|
||||
|
||||
Instructions for setting up Vanity Subdomains are now available in our [docs](https://supabase.com/docs/guides/platform/custom-domains#vanity-subdomains).
|
||||
|
||||
## Limitations
|
||||
|
||||
In this release, we still have some limitations that we're working through:
|
||||
|
||||
1. Edge Functions don't support custom domains or vanity subdomains yet, so you have to use the current endpoint to access them.
|
||||
2. Right now, you can only set up one custom domain or vanity subdomain per project.
|
||||
|
||||
We're working hard to fix these limitations, so stay tuned for future updates.
|
||||
|
||||
## More Launch Week 6
|
||||
|
||||
- [Day 1: New Supabase Docs, built with Next.js](https://supabase.com/blog/new-supabase-docs-built-with-nextjs)
|
||||
- [Day 2: Supabase Storage v2: Image resizing and Smart CDN](https://supabase.com/blog/storage-image-resizing-smart-cdn)
|
||||
- [Day 3: Multi-factor Authentication via Row Level Security Enforcement](https://supabase.com/blog/mfa-auth-via-rls)
|
||||
- [Launch Week 6 Hackathon](https://supabase.com/blog/launch-week-6-hackathon)
|
||||
- [Who We Hire at Supabase](https://supabase.com/blog/who-we-hire)
|
||||
- [pg_crdt - an experimental CRDT extension for Postgres](https://supabase.com/blog/postgres-crdt)
|
||||
@@ -0,0 +1,240 @@
|
||||
---
|
||||
title: 'Community Day'
|
||||
description: Wrapping up Launch Week 6 with contributors, partners, and friends.
|
||||
author: andrew_smith,jonmeyers_io,thor_schaeff,tyler_shukert
|
||||
image: lw6-community/community-day.png
|
||||
thumb: lw6-community/community-day.png
|
||||
tags:
|
||||
- launch-week
|
||||
- community
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
Supabase is a collaborative company. We work with, sponsor, and support as many open source tools as possible. The great thing about this approach is that each tool brings their own community and it's starting to feel like Supabase is really just a “community of communities”. Far too much has happened in the Supabase community in the last 3 months for us to possibly feature it all here, so here's a collection of the highlights!
|
||||
|
||||
## New Integrations
|
||||
|
||||
While we've been working hard on new features, the community has been developing a healthy [integrations marketplace](https://supabase.com/docs/guides/integrations/integrations). Since Supabase is just Postgres, any tool which works with Postgres works with Supabase (no additional changes required). Today we're adding a few major players to the roster:
|
||||
|
||||
### FlutterFlow
|
||||
|
||||
<iframe
|
||||
className="w-full"
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube-nocookie.com/embed/hw9Q-NjASbU?modestbranding=1&autohide=1&showinfo=0&controls=0"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
[FlutterFlow](https://flutterflow.io/) is a low-code builder for developing native mobile applications using Flutter. You can use the simple drag-and-drop interface to build your app faster than traditional development.
|
||||
|
||||
An integration between FlutterFlow and Supabase has been long awaited by the community. Starting today, you can build your apps with FlutterFlow and use Supabase for your backend!
|
||||
|
||||
This is just the beginning of our integration with FlutterFlow. We're working closely with the FlutterFlow team to deliver more features!
|
||||
|
||||
Watch John from the FlutterFlow team demonstrate how you can [get started with FlutterFlow and Supabase](https://youtu.be/TmViZsWEBY0).
|
||||
|
||||
### OneSignal
|
||||
|
||||
<iframe
|
||||
className="w-full"
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube-nocookie.com/embed/mw0DLwItue4?modestbranding=1&autohide=1&showinfo=0&controls=0"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
[OneSignal](https://onesignal.com/) is a messaging platform allowing you to deliver push notifications, in-app messages, SMS, and emails to your users.
|
||||
|
||||
Combining OneSignal with Supabase, you can send cloud messages to your users with any database change.
|
||||
|
||||
We have guides on how to get started with [Next.js and OneSignal](https://supabase.com/docs/guides/integrations/onesignal), and [Flutter and OneSignal](https://github.com/OneSignalDevelopers/onesignal-supabase-sample-integration-supabase).
|
||||
|
||||
In the video above, William from OneSignal will show you how you can easily integrate OneSignal with your Supabase app.
|
||||
|
||||
### NextAuth
|
||||
|
||||
<iframe
|
||||
className="w-full"
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube-nocookie.com/embed/EdYQ9fF-hz4?modestbranding=1&autohide=1&showinfo=0&controls=0"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
[NextAuth.js](https://next-auth.js.org/) is an open source authentication solution for [Next.js](https://nextjs.org/). It allows you to bring your own Database so that you own your user data.
|
||||
|
||||
The amazing NextAuth community who have built a [Supabase Adapter](https://next-auth.js.org/adapters/supabase) that makes it easy to use NextAuth.js with Supabase, and enables the use of [Row Level Security (RLS) policies](https://next-auth.js.org/adapters/supabase#enabling-row-level-security-rls).
|
||||
|
||||
In [this community spotlight video](https://youtube.com/EdYQ9fF-hz4), [Thang Huu Vu](https://twitter.com/thanghvu), the lead maintainer of NextAuth.js, gives an overview and brief history of NextAuth, followed by a tutorial on how to set up the new Supabase Adapter.
|
||||
|
||||
## Postgres Ecosystem
|
||||
|
||||
### PGroonga Multilingual Search extension
|
||||
|
||||

|
||||
|
||||
Over the past few years, we've had a number of people ask if we can add Full Text Search for other languages. PGroonga has been on our radar for a long time now, so when we reached out to them we were surprised to learn that _no_ Postgres platform currently supports them. Today we're fixing this by becoming the first.
|
||||
|
||||
PostgreSQL of course features excellent [Full Text Search](https://supabase.com/docs/guides/database/full-text-search) out-the-box, but it can be lacking if you need to search against languages with non-Roman characters such as Japanese, Chinese and so on. For this you can use the PGroonga extension! It's already installed on all new projects, just head to the [Database tab](https://app.supabase.com/project/_/database/extensions) on the dashboard and search or scroll for 'PGroonga' and hit enable to get started. Once enabled you can head to [their excellent documentation](https://pgroonga.github.io/tutorial/) on how to start making queries.
|
||||
|
||||
PGroonga is backed by a collection of incredible maintainers, contributors, and a broad and diverse [community](https://pgroonga.github.io/community/).
|
||||
|
||||
### PostgreSQL 15
|
||||
|
||||

|
||||
|
||||
The PostgreSQL community [released](https://www.postgresql.org/docs/current/release-15.html) version 15 (stable) in October 2022. All Projects created on the Supabase platform from today will be on version 15. If you want to upgrade one of your existing Projects, check out our [Upgrading](https://supabase.com/docs/guides/platform/migrating-and-upgrading-projects#upgrade-your-project) documentation, and if you want to see what's new, check out : XXX
|
||||
|
||||
Read [what's new in **Postgres 15?**](https://supabase.com/blog/new-in-postgres-15)
|
||||
|
||||
### PostgREST version 11 pre-release
|
||||
|
||||

|
||||
|
||||
PostgREST 11 is not wrapped up yet but you can try a pre-release locally with the Supabase CLI. Some of the new features include: spreading related tables, related orders and anti-joins. Read on more about these on:
|
||||
|
||||
[Learn more about PostgREST 11](https://supabase.com/blog/postgrest-11-prerelease)
|
||||
|
||||
## Made with Supabase
|
||||
|
||||

|
||||
|
||||
Perhaps you've seen [madewithsupabase.com](https://www.madewithsupabase.com/), but did you know that the site was made and is maintained by an [open source contributor](https://twitter.com/zernonia) (during our first hackathon)? Since then, there have been over 300 apps and websites listed. Here's some of our favourites:
|
||||
|
||||
### LottieFiles
|
||||
|
||||
LottieFiles built a [Mobile Motion Canvas](https://www.youtube.com/watch?v=QoN7l_Wq_S4) where you can create and share social stories, posts, stickers, & more.
|
||||
|
||||
### Mobbin
|
||||
|
||||
Mobbin use Supabase Auth to power their [UI/UX library of Mobile and Web screenshots](https://mobbin.com/browse/ios/apps). Last year they [migrated 200k users from Firebase to Supabase Auth](https://supabase.com/blog/mobbin-supabase-200000-users).
|
||||
|
||||
### sound.xyz
|
||||
|
||||
[sound.xyz](https://sound.xyz) is platform for artists to drop tracks to their fans first, users support the artists they love directly and stake their claim on being there before everyone else.
|
||||
|
||||
### Topaz
|
||||
|
||||
[Topaz](https://topaz.network/) is one of the fastest growing NFT networks and chose supabase as a backend to ensure they won't lose customers during peak traffic periods.
|
||||
|
||||
### Repeat.dev
|
||||
|
||||
[Repeat.dev](https://repeat.dev) is a platform for webhook based task automation. It has a built in scheduler, and can handle emails and pdf generation.
|
||||
|
||||
### Dots
|
||||
|
||||
[Dots](https://www.dots.community/) is a tool for interactive onboarding and support flows across Slack and Discord. Dots uses Supabase so they don't have to worry about managing a backend, and can focus on helping their users build amazing community experiences.
|
||||
|
||||
### Fleeting Notes
|
||||
|
||||
[Fleeting Notes](https://www.fleetingnotes.app/) allows you to take linked notes within your browser or on your phone, then sync the notes with Obsidian. They wrote an awesome guide on how they [migrated 1000+ users from Firebase to Supabase](https://fleetingnotes.app/posts/migrating-from-firebase-to-supabase/).
|
||||
|
||||
## Content Creators
|
||||
|
||||

|
||||
|
||||
One of the most exciting developments this year is the number of content creators and developers helping _others_ to get started with Supabase. Ultimately, this means more developers using Postgres. There's really too many of these awesome folks to feature, but here's a few who have gone above and beyond:
|
||||
|
||||
### Content Storm
|
||||
|
||||
More than 30+ creators featured in our [Content Storm](https://supabase.com/blog/the-supabase-content-storm). These folks all produced brand new content in the build up to Launch Week and dropped it simultaneously!
|
||||
|
||||
### Supaship
|
||||
|
||||

|
||||
|
||||
Supaship built a whole [Supabase course](https://supaship.io/). It's absolutely jam-packed with advice on how to get the most out of Supabase (and his meme game is absolute fire!).
|
||||
|
||||
### Jon Meyers for Egghead
|
||||
|
||||

|
||||
|
||||
Check out Jon's brand new, entirely free course on egghead to learn all about the Supabase Auth Helpers. Throughout the course, you will build a realtime chat application with Remix and learn how to:
|
||||
|
||||
- use Supabase server-side in loaders and actions
|
||||
- query and mutate data as an authenticated user with Supabase
|
||||
- sign users in with GitHub OAuth
|
||||
- enforce authorization with Row Level Security (RLS) policies
|
||||
- merge client and server state with realtime events
|
||||
- deploy a Remix app to Vercel
|
||||
|
||||
[Build a Realtime Chat App with Remix and Supabase](https://egghead.io/courses/build-a-realtime-chat-app-with-remix-and-supabase-d36e2618)
|
||||
|
||||
### Everything Svelte
|
||||
|
||||

|
||||
|
||||
[Everything Svelte](https://www.everythingsvelte.com/) is a Sveltekit and Supabase course by [Amy](https://twitter.com/selfteachme). You'll learn everything you need to know to build a modern web application.
|
||||
|
||||
> In Everything Svelte, participants will learn EVERYTHING they need to know to build a full-stack application in SvelteKit and Supabase. We cover topics such as Tailwind styles, Svelte animations and transitions, search, testing, creating components, managing state, Stripe integration, sending emails, GitHub Actions, database modeling, and deployment. Supabase, in particular, really shines as we leverage its authentication system, row level security, local storage, and database APIs.
|
||||
|
||||
## Getting started templates
|
||||
|
||||
### Base Jump
|
||||
|
||||
[usebasejump.com](https://usebasejump.com/) is "The fastest way to launch Supabase apps with NextJS". Basejump is open source. Contributions are both awesome and encouraged!
|
||||
|
||||
### supastarter
|
||||
|
||||
[supastarter.dev](https://supastarter.dev/) is the SaaS starter template for Next.js and Supabase. Save time and focus on your business with this scalable and production-ready starter template for your SaaS. It includes authentication, i18n, subscriptions, landing page and much more!
|
||||
|
||||
## Community libraries
|
||||
|
||||

|
||||
|
||||
The Supabase-Community org is home to 80(!) repos built and maintained by the community. It houses everything from [Client Libraries](https://github.com/supabase-community/#client-libraries), to [Auth Helpers](https://github.com/supabase-community/#supabase-auth-helpers), [Self Hosting tooling](https://github.com/supabase-community/#self-hosting), and [Utilities](https://github.com/supabase-community/#utilities).
|
||||
|
||||
An awesome recent addition from the AWS team is [supabase-on-aws](https://github.com/supabase-community/supabase-on-aws), a self-hosting starter kit for AWS.
|
||||
|
||||
## Community Growth
|
||||
|
||||

|
||||
|
||||
At the start of this year [@kiwicopple tweeted](https://twitter.com/kiwicopple/status/1478338320346267648) how far we had come as a community. Since then we've grown to one of the [most popular](https://github.com/search?q=stars%3A%3E42000&type=repositories) repos on GitHub (top 220) and have grown by:
|
||||
|
||||
- GitHub stars: 25K → 60K (across all repos)
|
||||
- Hosted Databases: 75K → 250K
|
||||
- Contributors: 271 → 750 (across all repos)
|
||||
- Launch Weeks: 3 → 6
|
||||
- Discord: 0 → 11k
|
||||
|
||||
## Giving back
|
||||
|
||||
At less than 3 years old we're still finding our feet, but one of the things we're most proud of is our contribution back to open source. Technically everyone working at Supabase is an open source contributor, so it's hard to quantify exactly the value we've been contributing to the open source ecosystem. To name a few of the more obvious ways:
|
||||
|
||||
- Paid out over $100,000 to contributors via [Open Collective](https://opencollective.com/supabase).
|
||||
- Invested in $500,000 into [OrioleDB](https://github.com/orioledb/orioledb) to accelerate the Table Access API in Postgres.
|
||||
- We financially sponsor open source tools, like [PostgREST](https://opencollective.com/supabase-postgrest) and [PGroonga](https://opencollective.com/clear-code), and research initiatives like [Elixir type system](https://twitter.com/josevalim/status/1535008937640181760?s=20&t=BLqeO2YpdhYfCZxgsmlNug).
|
||||
- We employ maintainers of the external tools which we use, including PostgREST and pgsodium (to work on those tools)!
|
||||
- Our team has contributed several patches to the Postgres core.
|
||||
- We help to drive open source adoption on tools like [Deno](https://deno.land/).
|
||||
- We collaborated with the [Snaplet](https://www.snaplet.dev/) team to develop and open source [Postgres WASM](https://supabase.com/blog/postgres-wasm).
|
||||
- And possibly the biggest: we've maintained our OSI-compliant stance. With the exception of some of our cloud-infra & billing code, the Supabase ecosystem is all MIT, Apache 2.0, or PostgreSQL licensed.
|
||||
|
||||
We plan to continue our open and collaborative trend. There are many companies working on exciting ideas in the database ecosystem, and there are far too many problems for the Supabase team to solve alone. If you're interested in working with us to solve these problems, don't ever hesitate to reach out.
|
||||
|
||||
## Getting involved
|
||||
|
||||
- Want to contribute? Dive into any of our [repos](https://github.com/supabase/) to get started.
|
||||
- Want to work in open source? We don't have many openings today, but keep this URL in mind in the future when you're job-hunting: [supabase.com/careers](https://supabase.com/careers)
|
||||
- Want to try out Supabase? [Join the Hackathon](https://supabase.com/blog/launch-week-6-hackathon) and win some nice prizes.
|
||||
- Want to support us? Just a [GitHub star](https://github.com/supabase/supabase) is enough ⭐.
|
||||
|
||||
## More Launch Week 6
|
||||
|
||||
- [Day 1: New Supabase Docs, built with Next.js](https://supabase.com/blog/new-supabase-docs-built-with-nextjs)
|
||||
- [Day 2: Supabase Storage v2: Image resizing and Smart CDN](https://supabase.com/blog/storage-image-resizing-smart-cdn)
|
||||
- [Day 3: Multi-factor Authentication via Row Level Security Enforcement](https://supabase.com/blog/mfa-auth-via-rls)
|
||||
- [Day 4: Supabase Wrappers, a Postgres FDW framework written in Rust](https://supabase.com/blog/postgres-foreign-data-wrappers-rust)
|
||||
- [Who We Hire at Supabase](https://supabase.com/blog/who-we-hire)
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
title: 'Launch Week 6: Wrap Up'
|
||||
description: "That's a wrap on Supabase Launch Week Day 6. Here's everything we shipped in one long blog post."
|
||||
author: paul_copplestone
|
||||
image: launch-week-6/wrap-up/og-wrap-up.png
|
||||
thumb: launch-week-6/wrap-up/og-wrap-up.png
|
||||
tags:
|
||||
- launch-week
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
That's a wrap on Supabase Launch Week Day 6. Here's everything we shipped in one long blog post.
|
||||
|
||||
## New Docs, built with Next.js
|
||||
|
||||

|
||||
|
||||
<Badge>Monday</Badge>
|
||||
|
||||
For a developer tool, documentation is more than a resource - it's part of the product. For the past two years at Supabase, this part of our product hasn't been great. Our new docs, built with Next.js, feature a completely new design, better navigation, and the promise of a fully-integrated experience.
|
||||
|
||||
- Read the [announcememt](/blog/new-supabase-docs-built-with-nextjs)
|
||||
- Watch the [recap on YouTube](https://www.youtube.com/watch?v=Q1Amk6iDlF8)
|
||||
- Check out [the new docs](https://supabase.com/docs)
|
||||
|
||||
## Storage v2: Image resizing and Smart CDN
|
||||
|
||||

|
||||
|
||||
<Badge>Tuesday</Badge>
|
||||
|
||||
We're introducing three new features for Supabase Storage: Image resizing, webhooks, and a Smart CDN. These features are designed to work together to deliver a next-gen image resizing system.
|
||||
|
||||
- Read the [announcement](https://supabase.com/blog/storage-image-resizing-smart-cdn)
|
||||
- Watch the [recap on YouTube](https://www.youtube.com/watch?v=NpEl20iuOtg)
|
||||
- Browse the docs for [Image Transformations](https://supabase.com/docs/guides/storage/image-transformations)
|
||||
- Browse the docs for our [Smart CDN](https://supabase.com/docs/guides/storage/cdn#smart-cdn-caching)
|
||||
- Discuss it on [HackerNews](https://news.ycombinator.com/item?id=33969076)
|
||||
|
||||
## Auth: Multi-factor Authentication with RLS
|
||||
|
||||

|
||||
|
||||
<Badge>Wednesday</Badge>
|
||||
|
||||
We released Multi Factor Authentication for you to build more secure applications. We built a unique twist so that you can use MFA within your Row Level Security Policies.
|
||||
|
||||
- Read the [announcement](/blog/mfa-auth-via-rls)
|
||||
- Watch the [recap on YouTube](https://www.youtube.com/watch?v=He7LI2mv9v0)
|
||||
- Browse the [docs](/docs/guides/auth/auth-mfa)
|
||||
- Discuss it on [HackerNews](https://news.ycombinator.com/item?id=33984104)
|
||||
|
||||
## Wrappers, a FDW framework for Postgres
|
||||
|
||||

|
||||
|
||||
<Badge>Thursday</Badge>
|
||||
|
||||
We announced [Supabase Wrappers](https://github.com/supabase/wrappers), a framework for building Postgres Foreign Data Wrappers (FDW) which connects Postgres to external systems. We're releasing Wrappers today in Alpha, with support for Firebase and Stripe. Wrappers for Clickhouse, BigQuery, and Airtable are under development.
|
||||
|
||||
- Read the [announcement](/blog/postgres-foreign-data-wrappers-rust)
|
||||
- Watch the [recap on YouTube](https://www.youtube.com/watch?v=He7LI2mv9v0)
|
||||
- Browse the [docs](https://supabase.github.io)
|
||||
- Discuss it on [HackerNews](https://news.ycombinator.com/item?id=34001493)
|
||||
|
||||
## Vault: secrets and encryption in Postgres
|
||||
|
||||

|
||||
|
||||
<Badge>Friday</Badge>
|
||||
|
||||
Vault is a new Postgres extension and accompanying Supabase UI that makes it safe and easy to store encrypted secrets and encrypt other stored data in your database. We're releasing it progressively across the platform and you can expect to see it appear in the Supabase Dashboard in the coming weeks.
|
||||
|
||||
- Read the [announcement](/blog/vault-now-in-beta)
|
||||
- Learn about [Transparent Column Encryption](/blog/transparent-column-encryption-with-postgres)
|
||||
|
||||
## pg_graphql v1: GraphQL in Postgres
|
||||
|
||||

|
||||
|
||||
<Badge>One more thing</Badge>
|
||||
|
||||
Today released pg_graphql 1.0 release and its general availability on our platform. pg_graphql is a PostgreSQL extension that allows you to query your database using GraphQL. It is the foundation of GraphQL support in the Supabase stack.
|
||||
|
||||
- Read the [announcement](/blog/pg-graphql-v1)
|
||||
- Browse the [GitHub repo](https://github.com/supabase/pg_graphql/)
|
||||
- View the [docs](/docs/guides/api#graphql-api)
|
||||
|
||||
## Postgres Point-in-Time Recovery
|
||||
|
||||

|
||||
|
||||
<Badge>One more thing</Badge>
|
||||
|
||||
Point In Time Recovery (PITR) is now enabled from the dashboard for Pro Projects. With PITR, you can restore your database at any specified time in the past.
|
||||
|
||||
- Read the [announcement](/blog/postgres-point-in-time-recovery)
|
||||
- Review the [Production Readiness docs](/docs/guides/platform/going-into-prod)
|
||||
|
||||
## Custom domains
|
||||
|
||||

|
||||
|
||||
<Badge>One more thing</Badge>
|
||||
|
||||
Custom domains allow you to use your own domain for your Supabase project. We've released custom domains as an upgrade option for all projects, and vanity domains for Pro projects.
|
||||
|
||||
- Read the [announcement](/blog/custom-domain-names)
|
||||
- Visit the [Custom Domains docs](/docs/guides/platform/custom-domains)
|
||||
|
||||
## pg_crdt: a Postgres extension for CRDTs
|
||||
|
||||

|
||||
|
||||
<Badge>One more thing</Badge>
|
||||
|
||||
We open-sourced an experimental extension for CRDTs, pg_crdt. If you're familiar with Yjs or Automerge, then check out the work we're doing here. It's a PostgreSQL extension that allows you to use CRDTs in your database.
|
||||
|
||||
- Read the [announcement](/blog/postgres-crdt)
|
||||
- Browse the [GitHub repo](https://github.com/supabase/pg_crdt)
|
||||
- Discuss it on [HackerNews](https://news.ycombinator.com/item?id=33931971)
|
||||
|
||||
## PostgreSQL 15: we support it
|
||||
|
||||

|
||||
|
||||
<Badge>One more thing</Badge>
|
||||
|
||||
All new projects created on the Supabase platform are now on version 15. The PostgreSQL community released version 15 (stable) in October 2022.
|
||||
|
||||
- Learn what's [new in PostgreSQL 15](/blog/new-in-postgres-15)
|
||||
- View the full PostgreSQL 15 [release notes](https://www.postgresql.org/docs/15/release-15.html)
|
||||
|
||||
## PostgREST 11: pre-release
|
||||
|
||||

|
||||
|
||||
<Badge>One more thing</Badge>
|
||||
|
||||
You can now test the PostgREST 11 pre-release locally with the Supabase CLI. Some of the new features include: spreading related tables, related orders and anti-joins.
|
||||
|
||||
- Learn more about [PostgREST 11](/blog/postgrest-11-prerelease)
|
||||
- Visit the [CLI docs](/docs/guides/resources/supabase-cli)
|
||||
|
||||
## Multilingual search in Postgres
|
||||
|
||||

|
||||
|
||||
<Badge>One more thing</Badge>
|
||||
|
||||
We've released a new multilingual Full Text Search extension, PGroonga. It's fast, it's open source, and it's available on our platform today. If you've ever wanted to build a search engine for your app, then check out PGroonga.
|
||||
|
||||
- Visit the [PGroonga website](https://pgroonga.github.io)
|
||||
- Read the [Extensions Docs](/docs/guides/database/extensions)
|
||||
|
||||
## Flutterflow for Flutter App Development
|
||||
|
||||
<iframe
|
||||
className="video-with-border w-full"
|
||||
width="640"
|
||||
height="385"
|
||||
src="https://www.youtube-nocookie.com/embed/hw9Q-NjASbU"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
|
||||
<br />
|
||||
|
||||
<Badge>Community Spotlight</Badge>
|
||||
|
||||
We partnered with the team at [Flutterflow](https://flutterflow.io/) to bring you a new way to build Flutter apps. Flutterflow is a low-code platform that allows you to build Flutter apps without writing any code. It's a great way to get started with Flutter and Supabase.
|
||||
|
||||
- Watch the [tutorial](https://www.youtube.com/watch?v=hw9Q-NjASbU)
|
||||
- Get Started with [Flutterflow](https://flutterflow.io/)
|
||||
|
||||
## OneSignal for Push Notifications
|
||||
|
||||
<iframe
|
||||
className="video-with-border w-full"
|
||||
width="640"
|
||||
height="385"
|
||||
src="https://www.youtube-nocookie.com/embed/mw0DLwItue4"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
|
||||
<br />
|
||||
|
||||
<Badge>Community Spotlight</Badge>
|
||||
|
||||
We partnered with the team at [OneSignal](https://onesignal.com/) to offer push notifications in Supabase. OneSignal is a messaging platform allowing you to deliver push notifications, in-app messages, SMS, and emails to your users.
|
||||
|
||||
- Watch the [tutorial](https://www.youtube.com/watch?v=mw0DLwItue4)
|
||||
- Get Started with [OneSignal](https://onesignal.com/)
|
||||
|
||||
## Getting Started
|
||||
|
||||
Get started today with Supabase. It's free and open source.
|
||||
|
||||
- [Like some memes on Twitter](https://twitter.com/supabase)
|
||||
- [Star us on GitHub](https://github.com/supabase/supabase)
|
||||
- [Join the Discord](https://discord.supabase.com)
|
||||
- [Read the docs](https://supabase.com/docs)
|
||||
- [Sign up](https://app.supabase.io/)
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: What's new in Postgres 15?
|
||||
description: Describes the release of Postgres 15, new features and reasons to use it
|
||||
author: pavel
|
||||
image: lw6-community/pg15.png
|
||||
thumb: lw6-community/pg15.png
|
||||
tags:
|
||||
- postgres
|
||||
- launch-week
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
The PostgreSQL community [released](https://www.postgresql.org/docs/current/release-15.html) version 15 (stable) in October 2022. Let's review some of the new features.
|
||||
|
||||
## **The permission model is more secure**
|
||||
|
||||
`CREATE` permission is revoked from all users except the database owner. It makes permission assigning more tunable ([details](https://www.postgresql.org/docs/15/ddl-schemas.html#DDL-SCHEMAS-PATTERNS)). And for the migrated database don't forget to revoke `CREATE` permission on the public schema manually to fit the new policy.
|
||||
|
||||
There is a useful option `CREATE VIEW .. WITH security_invoker=on` to create a view that uses permissions of a view caller rather than a view creator to access underlying tables. With this, you should not worry that a user that doesn't have access to a table could see its data through a view.
|
||||
|
||||
## **Performance speed-up**
|
||||
|
||||
Sorting rows of data is a frequent operation inside the PostgreSQL code. It is used not only when you use the ORDER BY clause, but also in indices creation, table partitioning, etc. Furthermore, sorting is one of the most algorithmically expensive operations. So increasing speed up to 4 times of both in-memory and on-disk sorts in version 15 is one of the main reasons to upgrade.
|
||||
|
||||
Window functions performance is also improved.
|
||||
|
||||
## The **MERGE operation was introduced**
|
||||
|
||||
A new `MERGE` operation is to modify target table data according to the provided source with many available conditional processing options. This allows data processing without writing procedural language functions or complex CTE queries. Essentially merge can work as conditional delete/insert/update and also this makes PostgreSQL syntax implementation closer to SQL:2008 standard. For details on MERGE uses see: [https://www.postgresql.org/docs/15/sql-merge.html](https://www.postgresql.org/docs/15/sql-merge.html)
|
||||
|
||||
## **New features for logical replication**
|
||||
|
||||
Logical replication is one of the fast-developing parts of PostgreSQL. Since version 15 two-phase commits are supported in logical replication. Also now you can choose parts of table data to logically replicate using i.e. some sets of rows and columns. In the case of replication conflict i.e. replicated data violate some constraint or there is a permission violation on a subscriber server user can choose to skip the conflicting transaction or disable the subscription until the user can decide how to handle the conflict.
|
||||
|
||||
## **Increase parallelization**
|
||||
|
||||
Parallel execution of queries using several worker processes is another part of PostgreSQL that develops fast. In version 15 `SELECT DISTINCT` queries that drop duplicate rows from output can improve performance by using parallel workers.
|
||||
|
||||
## **JSON logging**
|
||||
|
||||
Output log data as JSON structure helps logs processing and structured output on the client's side.
|
||||
|
||||
## **Compressing options**
|
||||
|
||||
In version 15 _Zstandard_ and _ZL4_ can be chosen instead of _pglz_ as a WAL compression method. WAL compression is useful to decrease disk space usage while safeguarding data integrity.
|
||||
|
||||
When you make a database backup with the provided pg*basebackup extension you can also choose \_Zstandard* or _ZL4_ to compress database backups on the server side, to transfer less data over the network.
|
||||
|
||||
## **New POSIX regexp functions**
|
||||
|
||||
There are several new regular expressions functions that you may find useful for strings processing i.e. regexp_substr(), regexp_count(), etc. [Details](https://www.postgresql.org/docs/15/functions-matching.html#FUNCTIONS-POSIX-REGEXP)
|
||||
|
||||
## **Full Changelog**
|
||||
|
||||
You can find a full changelog and full list of new features in the [official changelog](https://www.postgresql.org/docs/15/release-15.html#id-1.11.6.6.5).
|
||||
|
||||
## **Should you upgrade to PG15?**
|
||||
|
||||
The main reason to switch to a new PostgreSQL version is the large number of bug fixes that each new version delivers. Bug fixes aren't as prominent as new features or performance improvements, but they often have a greater impact. While some bug fixes are applied to previous versions, these changes require modifications to the ABI, interface functions, and system catalog structures, often not possible in an already-released major version.
|
||||
|
||||
So, the TL;DR: yes, you should update to improve stability.
|
||||
|
||||
Upgrading is safe thanks to the half-year stabilization period between April's code freeze of every new feature and October's stable release. Generally, you'd rather find a bug in an old version than in a new one.
|
||||
|
||||
Before choosing to upgrade, don't forget to check the list of rare cases that need [manual actions](https://www.postgresql.org/docs/current/release-15.html#id-1.11.6.6.4) before or after the upgrade.
|
||||
|
||||
- If you're self-hosting, upgrading between major PostgreSQL is easily done with [pg_upgrade](https://www.postgresql.org/docs/current/pgupgrade.html) utility or a set of recommended practices in the [official docs](https://www.postgresql.org/docs/current/upgrading.html).
|
||||
- If you're starting a new project on Supabase, you'll already be on PG15.
|
||||
- If you want to upgrade an existing project, check out our [Upgrading](https://supabase.com/docs/guides/platform/migrating-and-upgrading-projects#upgrade-your-project) documentation.
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: pg_graphql v1.0
|
||||
description: Announcing the v1.0 release of pg_graphql
|
||||
author: oli_rice
|
||||
image: launch-week-6/pggraphql/og-pg-graphql.png
|
||||
thumb: launch-week-6/pggraphql/og-pg-graphql.png
|
||||
tags:
|
||||
- graphql
|
||||
- postgres
|
||||
- launch-week
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
Today we're announcing the 1.0 release of pg_graphql and its general availability on our platform. pg_graphql is a PostgreSQL extension that allows you to query your database using GraphQL. It is the foundation of GraphQL support in the Supabase stack.
|
||||
|
||||
Since our first platform release, v0.2.1, the feature set of pg_graphql has steadily grown and stabilized. Despite being a pre-1.0, we've been extremely cautious with each new feature and have yet to introduce a backwards-incompatible change. With the 1.0 release we're formalizing that guarantee, subject to the SemVer spec.
|
||||
|
||||
## Background
|
||||
|
||||
pg_graphql was created to satisfy an extreme set of constraints. Mainly, [Supabase free tier](https://supabase.com/pricing) projects run on servers with 1 GB of memory. On those servers, we squeeze tuned versions of [PostgreSQL](https://github.com/supabase/postgres), [PostgREST](https://postgrest.org/en/stable/), and [GoTrue](https://github.com/supabase/gotrue). Every megabyte consumed by something that isn't PostgreSQL is another chance for an index to fall out of memory, or a large query to fail.
|
||||
|
||||
Our philosophy when adding to the stack is to use existing open source tools wherever possible. We surveyed the available GraphQL → SQL options and found some excellent candidates in [Hasura](https://hasura.io/) and [Graphile](https://www.graphile.org/). Both support the set of features we're interested in, but consume significantly more memory than we could sacrifice on the free tier. Realizing that, we searched for an architecture that could meet our runtime constraints and performance requirements.
|
||||
|
||||
## Architecture
|
||||
|
||||
Our first prototype of pg_graphql PostgreSQL extension had a parser written in C (libgraphqlparser) with all business logic of transpiling GraphQL to SQL **written** in SQL. We exposed the extensions sole SQL function `graphql.resolve(...)` over HTTP using PostgREST's [RPC functionality](https://postgrest.org/en/stable/api.html#s-procs). With this approach, the memory footprint was too small to measure when accessed over HTTP. While memory-use sent us down this path, we discovered that leaning into Postgres primitives lead to some incredible synergies. To name a few:
|
||||
|
||||
### Security
|
||||
|
||||
Since data are accessed through a standard, unprivileged, SQL function, Postgres role permissions and Row Level Security (RLS) policies work exactly like they do in Postgres. Define your security model once, and it applies everywhere: SQL, REST, Realtime, and GraphQL
|
||||
|
||||
### Always up-to-date
|
||||
|
||||
No separate process means no roundtrip time. Inspecting the database's schema does not require any caching and is always in-sync. Knowing that the GraphQL and SQL schemas are aligned give us the confidence to compile GraphQL requests of any complexity into exactly one SQL query, [solving the N+1 query problem](https://medium.com/the-marcy-lab-school/what-is-the-n-1-problem-in-graphql-dd4921cb3c1a) and producing high throughput.
|
||||
|
||||
### Scaling
|
||||
|
||||
As an extension, pg_graphql's performance scales directly with the size of the database. When a user's performance needs grow, upgrading the database instance also scales up GraphQL throughput with no external processes to manage.
|
||||
|
||||
### ACID - Atomicity, Consistency, Isolation, and Durability
|
||||
|
||||
Databases have strong ACID guarantees. Being embedded in the database lets us claim those guarantees through the GraphQL API. For example, if any part of a multi-mutation GraphQL request fails, the entire request can roll back to leave the database in a consistent state.
|
||||
|
||||
## From SQL to Rust
|
||||
|
||||
Our pure [SQL implementation of the pg_graphql transpiler](https://github.com/supabase/pg_graphql/blob/bd0283718abaf329d98c69808f862594e9df5edc/pg_graphql--0.4.0.sql) carried us from v0.0.1 to v0.4.0. Ultimately, we started to feel some pain, although probably not in the way you'd expect.
|
||||
|
||||
Opinions about business logic in SQL are notoriously split. In my view, there are some foot guns, but:
|
||||
|
||||
- SQL has types
|
||||
- SQL has functions
|
||||
- Functions are composable
|
||||
|
||||
Ergo, keep your functions pure and you've got a strongly typed functional programming paradigm. Taking that approach, the developer experience for pg_graphql was surprisingly excellent. The feedback cycle with [pg_regress](https://www.postgresql.org/docs/9.1/regress.html) is fast, and occasionally the type system even caught a bug or two. What more could you ask for?
|
||||
|
||||
Somewhere around the 4,000 line count, things started to bog down. The rough shape of the codebase was coming into view, which made it clear where we could further extract common patterns into functions. At this point I learned two things:
|
||||
|
||||
- SQL functions are “pass by value” (copy) so passing state around can get expensive
|
||||
- The query planner tends to stop inlining functions when calls are deeply nested
|
||||
|
||||
Combine those two facts with our GraphQL AST being a big ball of JSONB, and the GitHub issues about slow schema introspection on free tier started to trickle in.
|
||||
|
||||
Enter [pgx](https://github.com/tcdi/pgx)…
|
||||
|
||||
pgx is a rust framework for building Postgres extensions. It has a very polished onboarding experience and (as you would expect) full SQL interop. That made it straightforward to initially sprinkle some rust on pg_graphql's hotspots. When that went well, we ultimately rewrote pg_graphql from the ground up as a pure Rust project for the v0.5.0 release.
|
||||
|
||||
One of rust's main selling points is zero cost abstractions. That means high-level concepts like generics incur no runtime penalty. Transitioning from SQL's expensive abstractions to rust's zero cost abstractions has been hugely satisfying as it enabled refactoring the codebase into a more maintainable state. Development velocity is up. Code quality is up. Performance is WAY up.
|
||||
|
||||
As of 1.0, overhead introduced by pg_graphql is sub 300 *micro*seconds per request on free-tier hardware. When executed from SQL we see ~1060 queries per second per connection (no parallelism). If we include the entire Auth + HTTP stack, free tier can handle ~645 requests per second. On larger instance the stack handles upwards of 10k requests / second.
|
||||
|
||||
```text
|
||||
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
|
||||
Server Software: postgrest/10.1.1
|
||||
Document Path: /graphql/v1
|
||||
Concurrency Level: 10
|
||||
Time taken for tests: 3.099 seconds
|
||||
Complete requests: 2000
|
||||
Requests per second: 645.40 [#/sec] (mean)
|
||||
Time per request: 15.494 [ms] (mean)
|
||||
Time per request: 1.549 [ms] (mean, across all concurrent requests)
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
While v1.0 is an important milestone for stability, there's plenty of room to expand the feature set. For example, some commonly requested features on the immediate roadmap are:
|
||||
|
||||
- Extended filtering options
|
||||
- `startsWith` for the `String` type
|
||||
- nestable `and`/ `or` blocks
|
||||
- Support for user-defined functions
|
||||
- Support for views
|
||||
|
||||
Longer term we look forward to experimenting with more ambitions features like an API for migrations, integrations with 3rd-party services through [supabase/wrappers](https://github.com/supabase/wrappers), and a scalable solution for subscriptions.
|
||||
|
||||
## More Launch Week 6
|
||||
|
||||
- [Day 1: New Supabase Docs, built with Next.js](https://supabase.com/blog/new-supabase-docs-built-with-nextjs)
|
||||
- [Day 2: Supabase Storage v2: Image resizing and Smart CDN](https://supabase.com/blog/storage-image-resizing-smart-cdn)
|
||||
- [Day 3: Multi-factor Authentication via Row Level Security Enforcement](https://supabase.com/blog/mfa-auth-via-rls)
|
||||
- [Launch Week 6 Hackathon](https://supabase.com/blog/launch-week-6-hackathon)
|
||||
- [Who We Hire at Supabase](https://supabase.com/blog/who-we-hire)
|
||||
- [pg_crdt - an experimental CRDT extension for Postgres](https://supabase.com/blog/postgres-crdt)
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: 'Point in Time Recovery is now available for Pro projects'
|
||||
description: "We're making PITR available for more projects, with a new Dashboard UI that makes it simple to use."
|
||||
author: angelico_de_los_reyes
|
||||
image: launch-week-6/pitr/og-pitr.png
|
||||
thumb: launch-week-6/pitr/og-pitr.png
|
||||
tags:
|
||||
- launch-week
|
||||
- postgres
|
||||
- backups
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
When we announced Point-in-Time a few months ago, it was only available for Enterprise customers. Today, we're making PITR available for more projects, with a new Dashboard UI that makes it simple to use.
|
||||
|
||||
Pro-tier projects created from August 2022 onwards (excluding Northeast Asia / Seoul region), can now enable Point In Time Recovery (PITR) from the dashboard. Projects must have the "Small" compute add-on to ensure that PITR operates smoothly behind the scenes.
|
||||
|
||||
For older projects, don't worry - we're working on making it available for everybody. In the meantime, if you would like to have PITR enabled for your ineligible project, reach out directly [here](https://app.supabase.com/support/new), or stay tuned for further updates.
|
||||
|
||||
## What is Point in Time Recovery?
|
||||
|
||||

|
||||
|
||||
Point in Time Recovery provides the ability for a database to be restored at any specified point in time. This is useful when averting disasters, for all those times that a, _cough_, natural calamity strikes (like accidentally dropping a table). With PITR, the database can be restored to a state it was in mere seconds before trouble.
|
||||
|
||||
All of this is made possible by a combination of physical backups and archives from the [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html). Physical backups provide a snapshot of the underlying directory of the database, while WAL files contain records of every change made in the database.
|
||||
|
||||
Under the hood, we use [WAL-G](https://github.com/wal-g/wal-g), an open source archival and restoration tool, to handle all aspects of PITR. On a daily basis, WAL-G takes a snapshot of the database and sends it to our storage servers. Throughout the day, as database transactions occur, WAL files are generated and uploaded to archived.
|
||||
|
||||
## Should I enable PITR?
|
||||
|
||||

|
||||
|
||||
Consider your Recovery Point Objective (RPO) when deciding whether to enable Point in Time Recovery. RPO is the threshold for how much data, measured in time, a business could lose when disaster strikes. This is dependent on a business and its underlying requirements. The agreed upon RPO would be a deciding factor in choosing which solution best fits a project.
|
||||
|
||||
While all Pro-tier projects and above are backed up on a daily basis, this means that at the worst case, a project could lose up to 24 hours worth of data if disaster hits at the most inopportune time. With Point in Time Recovery however, backups are made at much shorter intervals, shortening the RPO. WAL files are backed up at two minute intervals. This could be faster if it hits a certain file threshold before the the two minute mark.
|
||||
|
||||
## Getting started
|
||||
|
||||
Enabling Point in Time Recovery could be done [in the Dashboard](https://app.supabase.com/project/_/settings/billing/update/pro). Retention for backups used by PITR is set to up to 7 days by default but could be increased to up to 28 days via self-serve.
|
||||
|
||||
## More Launch Week 6
|
||||
|
||||
- [Day 1: New Supabase Docs, built with Next.js](https://supabase.com/blog/new-supabase-docs-built-with-nextjs)
|
||||
- [Day 2: Supabase Storage v2: Image resizing and Smart CDN](https://supabase.com/blog/storage-image-resizing-smart-cdn)
|
||||
- [Day 3: Multi-factor Authentication via Row Level Security Enforcement](https://supabase.com/blog/mfa-auth-via-rls)
|
||||
- [Launch Week 6 Hackathon](https://supabase.com/blog/launch-week-6-hackathon)
|
||||
- [Who We Hire at Supabase](https://supabase.com/blog/who-we-hire)
|
||||
- [pg_crdt - an experimental CRDT extension for Postgres](https://supabase.com/blog/postgres-crdt)
|
||||
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: PostgREST 11 pre-release
|
||||
description: Describes new features of PostgREST 11 pre-release
|
||||
author: steve_chavez
|
||||
image: lw6-community/postgrest.png
|
||||
thumb: lw6-community/postgrest.png
|
||||
tags:
|
||||
- postgres
|
||||
- launch-week
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
PostgREST 11 is not wrapped up yet, however a pre-release with the **[latest features and fixes](https://github.com/PostgREST/postgrest/releases/tag/v10.1.1.20221212)**
|
||||
is available on the Supabase CLI.
|
||||
|
||||
In this blog post we'll cover some of the improved querying capabilities: spreading related tables, related orders and anti-joins.
|
||||
|
||||
## Spreading related tables
|
||||
|
||||
Very often the way we structure a database is not the way we want to present it to the frontend application. For example, let's assume we have a `films` and `technical_specs` tables and
|
||||
they form a one-to-one relationship.
|
||||
|
||||
Using PostgREST resource embedding, we can query them in one request like so
|
||||
|
||||
<small>From HTTP:</small>
|
||||
|
||||
```http
|
||||
GET /films?select=title,technical_specs(camera,laboratory,sound_mix)
|
||||
```
|
||||
|
||||
<small>or JavaScript:</small>
|
||||
|
||||
```jsx
|
||||
const { data, error } = await supabase.from('films').select(`
|
||||
title,
|
||||
technical_specs (
|
||||
camera, laboratory, duration
|
||||
)
|
||||
`)
|
||||
```
|
||||
|
||||
<small>Response:</small>
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "Pulp Fiction",
|
||||
"technical_specs": {
|
||||
"camera": "Arriflex 35-III",
|
||||
"laboratory": "DeLuxe, Hollywood (CA), USA (color)",
|
||||
"duration": "02:34:00"
|
||||
}
|
||||
},
|
||||
"..."
|
||||
]
|
||||
```
|
||||
|
||||
But we'd like to present a “flattened” result to the frontend, without the `technical_specs` object. For this we could create a new database view or function that shapes the json the way we want, but creating extra database objects is not always convenient.
|
||||
|
||||
Using the new “spread” operator(syntax borrowed from [JS](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax)), we can expand a related table columns and remove the nested object.
|
||||
|
||||
<small>From HTTP:</small>
|
||||
|
||||
```jsx
|
||||
GET /films?select=title,...technical_specs(camera,laboratory,duration)
|
||||
```
|
||||
|
||||
<small>or JavaScript:</small>
|
||||
|
||||
```jsx
|
||||
const { data, error } = await supabase.from('films').select(`
|
||||
title,
|
||||
...technical_specs (
|
||||
camera, laboratory, duration
|
||||
)
|
||||
`)
|
||||
```
|
||||
|
||||
<small>Response:</small>
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "Pulp Fiction",
|
||||
"camera": "Arriflex 35-III",
|
||||
"laboratory": "DeLuxe, Hollywood (CA), USA (color)",
|
||||
"duration": "02:34:00"
|
||||
},
|
||||
"..."
|
||||
]
|
||||
```
|
||||
|
||||
This only works for one-to-one and many-to-one relationships for now but we're looking at ways to remove this restriction.
|
||||
|
||||
## Order by related tables
|
||||
|
||||
It's also a common use case to order a table by a related table column. For example, suppose you'd like to order `films` based on the `technical_specs.duration` column.
|
||||
|
||||
You can now do it like so:
|
||||
|
||||
<small>From HTTP:</small>
|
||||
|
||||
```http
|
||||
GET /films?select=title,...technical_specs(duration)&order=technical_specs(duration).desc
|
||||
```
|
||||
|
||||
<small>or JavaScript:</small>
|
||||
|
||||
```jsx
|
||||
const { data, error } = await supabase
|
||||
.from('films')
|
||||
.select(`
|
||||
title,
|
||||
...technical_specs (
|
||||
duration
|
||||
)
|
||||
`)
|
||||
.order('technical_specs(duration)', { descending: true }))
|
||||
```
|
||||
|
||||
<small>Response:</small>
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "Amra Ekta Cinema Banabo",
|
||||
"duration": "21:05:00"
|
||||
},
|
||||
{
|
||||
"title": "Resan",
|
||||
"duration": "14:33:00"
|
||||
},
|
||||
"..."
|
||||
]
|
||||
```
|
||||
|
||||
Similarly to spreading related tables, this only works for one-to-one and many-to-one relationships.
|
||||
|
||||
## Anti-Joins
|
||||
|
||||
To do the equivalent of a left anti-join, you can now filter the rows where the related table is `null`.
|
||||
|
||||
<small>From HTTP:</small>
|
||||
|
||||
```http
|
||||
GET /films?select=title,nominations()&nominations=is.null
|
||||
```
|
||||
|
||||
<small>or JavaScript:</small>
|
||||
|
||||
```jsx
|
||||
const { data, error } = await supabase
|
||||
.from('films')
|
||||
.select(`
|
||||
title,
|
||||
nominations()
|
||||
`)
|
||||
.is('nominations', null))
|
||||
```
|
||||
|
||||
<small>Response:</small>
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "Memories of Murder"
|
||||
},
|
||||
{
|
||||
"title": "Rush"
|
||||
},
|
||||
{
|
||||
"title": "Groundhog Day"
|
||||
},
|
||||
"..."
|
||||
]
|
||||
```
|
||||
|
||||
Note that `nominations` doesn't select any columns so they don't show on the resulting response.
|
||||
|
||||
The equivalent of an inner join can be done by filtering the rows where the related table is `not null`.
|
||||
|
||||
```http
|
||||
GET /films?select=title,nominations(rank,...competitions(name))&nominations=not.is.null
|
||||
```
|
||||
|
||||
```jsx
|
||||
const { data, error } = await supabase
|
||||
.from('films')
|
||||
.select(
|
||||
`
|
||||
title,
|
||||
nominations(rank,...competitions(name))
|
||||
`
|
||||
)
|
||||
.not('nominations', 'is', null)
|
||||
```
|
||||
|
||||
<small>Response:</small>
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "Pulp Fiction"
|
||||
"nominations": [
|
||||
{"rank": 1, "name": "Palme d'Or"},
|
||||
{"rank": 1, "name": "BAFTA Film Award"},
|
||||
{"..."}
|
||||
]
|
||||
},
|
||||
"..."
|
||||
]
|
||||
```
|
||||
|
||||
This was already possible with the `!inner` modifier([introduced on PostgREST 9](https://supabase.com/blog/postgrest-9#resource-embedding-with-inner-joins))
|
||||
but the `not null` filter is more flexible and can be used with an [or filter](https://supabase.com/docs/reference/javascript/or) to combine related tables' conditions.
|
||||
|
||||
## Try it out
|
||||
|
||||
This pre-release is not deployed to Supabase cloud but you can try it out locally with the [Supabase CLI](https://supabase.com/docs/reference/cli/introduction).
|
||||
|
||||
```bash
|
||||
$ supabase start
|
||||
```
|
||||
|
||||
Please try it and report any bugs, suggestions or ideas!
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: 'Supabase Vault is now in Beta'
|
||||
description: 'A Postgres extension to store encrypted secrets and encrypt data.'
|
||||
author: michel
|
||||
image: launch-week-6/vault/og-vault.png
|
||||
thumb: launch-week-6/vault/og-vault.png
|
||||
tags:
|
||||
- launch-week
|
||||
- postgres
|
||||
- encryption
|
||||
date: '2022-12-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
During our last Launch Week we [announced](/blog/supabase-vault) Supabase Vault as our “one more thing”. Today we're releasing it progressively across the platform.
|
||||
|
||||
Vault is a new Postgres extension and accompanying Supabase UI that makes it safe and easy to store encrypted secrets and encrypt other stored data in your database. This foundation opens up a lot of possibilities for Postgres that go beyond what is available in a stock distribution. From a product perspective we're grouping various features under the “Vault banner”. Let's explore a few of these features.
|
||||
|
||||
## Secrets Management
|
||||
|
||||
Practically speaking, 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, but available in decrypted form through a Postgres view so that the secrets can be used by applications from SQL. Because the secrets are stored encrypted and authenticated, any backups or replication streams also preserve this encryption in a way that can't be forged.
|
||||
|
||||
We've created a dashboard UI for the Vault that makes storing secrets easy. Click a button and type in your secret, optionally create a new key that is referenced by id (or use the existing default), and submit. Your secret is now stored on disk encrypted using the specified key id.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/images/blog/launch-week-6/vault/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:** encryption keys are used to encrypt data inside your database. Fun fact: the Secrets you store in the Vault are encrypted with an Encryption Key which we set up by default and is not accessible to SQL or stored in your database alongside the same data it is used to encrypt. You can create different Encryption Keys for different purposes, for example: one for encrypting user-data, and another for application-data.
|
||||
|
||||
## Transparent Column Encryption (TCE)
|
||||
|
||||
Our recent [blog post](/blog/transparent-column-encryption-with-postgres) describes TCE in-depth. TCE is one of the safest ways to encrypt your data so that it doesn't leak into logs and backups, as well as providing your users with row-level authenticated encryption. TCE is the foundational feature of the Vault, but you can use it on your own tables if you choose to if the Vault isn't sufficient for your needs, for example if you have multiple tables that you wish to have encrypted columns. Any Postgres value that can be cast to `text` or `bytea` can use TCE to encrypt the data that is stored to disk.
|
||||
|
||||
### Encrypting columns
|
||||
|
||||
In the “New Column” flow on the Dashboard, you can select that a `text` or `bytea` column is encrypted, and select an existing key id or create a new one. This is functionally identical to the Vault above, but you can apply it to any of your existing tables. In a sense the Vault is a pre-created table and UI for you to get started quickly storing secrets, and to be a centralized point for “global” secrets management, but your not stuck with just that, you can encrypt multiple columns in multiple tables, how you want to store your secret data can be entirely up to you.
|
||||
|
||||

|
||||
|
||||
Once you've setup an encrypted column, just insert data into the table like you would any other table. If you put in an email address for example, you will see that what is stored is not an email at all, but an encrypted value.
|
||||
|
||||

|
||||
|
||||
Decrypted data can be accessed by a special view that is automatically created whenever you create an encrypted column on a table. This view decrypts the data row-by-row as you access it. By default this view is called `decrypted_<your-table-name>`, so in the example provided, the decryption view for the `profiles` table is `decrypted_profiles`. In addition to the existing `emails` column, there is a new column in the view called `decrypted_emails` that contains the decrypted email value. It's that simple!
|
||||
|
||||

|
||||
|
||||
## Deep Dive on How The Vault works
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
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>
|
||||
|
||||
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.
|
||||
|
||||
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. “Authenticated Encryption” means that in addition to the data being encrypted, it is also “signed” so that it cannot be forged, the decryption function verifies that the signature is valid **before decrypting the value**. “Associated Data” means that in addition to signing the secret, you can include any other columns in your row in the signature computation, “associating” those columns with the secret. This doesn't encrypt those other columns, but it does ensure that **they are authentic** and cannot be forged because they are included in the secret's signature. So you know when you restore your database dumps, not only are the secrets safe, but also the “associated” columns are also authentic and unforged.
|
||||
|
||||
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 refers to a 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 would be no point in storing the encryption key in the database alongside the encrypted data, this would be like locking your front door but leaving the key in the lock! Storing the key outside the database fixes this issue.
|
||||
|
||||
You might be wondering, ok then where are the keys stored? Supabase creates and manages the root keys from which all key ids are derived in our internal customer backend systems. We keep this key safe and separate from your data, and provide an alternate dashboard endpoint for accessing the key if you want to decrypt your data outside of Supabase.
|
||||
|
||||
## Future possibilities
|
||||
|
||||
Privacy is becoming one of the most important features in a modern product. Supabase's embrace of the pgsodium extension aims to make this simple. Some of the possibilities we are looking into are:
|
||||
|
||||
- **End-to-end encryption**: give your users the ability to encrypt their personal data so even you, the developer cannot access it, using the libsodium [encrypted streams](https://doc.libsodium.org/secret-key_cryptography/secretstream) API that is exposed by pgsodium. After exchanging keys, parties can stream unlimited amounts of data from peer to peer without being intercepted by any party in between, including Supabase.
|
||||
- **Group encryption**: have you ever joined a group on Whatsapp and been frustrated that you can't read the previous messages? That's because group encryption is hard. We hope to make that easier using new algorithms like [signcryption](https://github.com/jedisct1/libsodium-signcryption) to support multi-party encrypted messages that can be easily encoded into a token format and use to support streaming encryption keys.
|
||||
- **Public Key Management:** Public Key Encryption can be hard, but pgsodium makes it easier by exposing all of the public key encryption functions that are supported by libsodium. Making and distributing key pairs is now easy, no need to run arcane GPG scripts or mess with SSL libraries. libsodium contains state of the art public key encryption, and by extension so does pgsodium. These features are available now on the Supabase platform and offer a lot of possibilities to unshackle developers from other confusing and inadequate solutions available today.
|
||||
|
||||
### Using the Vault
|
||||
|
||||
You'll see the Vault appear in the Dashboard over the next month. If you want to use Vault today, email growth@supabase.com and we'll enable it on your account. Vault is available for all customers (including the Free-tier).
|
||||
|
||||
## More Launch Week 6
|
||||
|
||||
- [Day 1: New Supabase Docs, built with Next.js](https://supabase.com/blog/new-supabase-docs-built-with-nextjs)
|
||||
- [Day 2: Supabase Storage v2: Image resizing and Smart CDN](https://supabase.com/blog/storage-image-resizing-smart-cdn)
|
||||
- [Day 3: Multi-factor Authentication via Row Level Security Enforcement](https://supabase.com/blog/mfa-auth-via-rls)
|
||||
- [Launch Week 6 Hackathon](https://supabase.com/blog/launch-week-6-hackathon)
|
||||
- [Who We Hire at Supabase](https://supabase.com/blog/who-we-hire)
|
||||
- [pg_crdt - an experimental CRDT extension for Postgres](https://supabase.com/blog/postgres-crdt)
|
||||
@@ -1,206 +0,0 @@
|
||||
import { Button, Form, Input, Listbox } from 'ui'
|
||||
import { useState } from 'react'
|
||||
import countries from '~/data/Countries.json'
|
||||
import supabase from '~/lib/supabase'
|
||||
|
||||
const INITIAL_VALUES = {
|
||||
companyName: '',
|
||||
contactFirstName: '',
|
||||
contactLastName: '',
|
||||
contactEmail: '',
|
||||
contactPhone: '',
|
||||
companySize: '',
|
||||
details: '',
|
||||
country: 'US',
|
||||
}
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
|
||||
if (!values.companyName) {
|
||||
errors.companyName = 'Required'
|
||||
}
|
||||
|
||||
if (!values.contactFirstName) {
|
||||
errors.contactFirstName = 'Required'
|
||||
}
|
||||
|
||||
if (!values.contactLastName) {
|
||||
errors.contactLastName = 'Required'
|
||||
}
|
||||
|
||||
if (!values.contactEmail) {
|
||||
errors.contactEmail = 'Required'
|
||||
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.contactEmail)) {
|
||||
errors.contactEmail = 'Invalid email address'
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const EnterpriseContactForm = () => {
|
||||
const [formSubmitted, setFormSubmitted] = useState<boolean>(false)
|
||||
|
||||
const handleFormSubmit = async (values: typeof INITIAL_VALUES, { resetForm }: any) => {
|
||||
const { error } = await supabase.from('enterprise_contacts').insert([
|
||||
{
|
||||
company_name: values.companyName,
|
||||
contact_name: `${values.contactFirstName} ${values.contactLastName}`,
|
||||
contact_first_name: values.contactFirstName,
|
||||
contact_last_name: values.contactLastName,
|
||||
contact_email: values.contactEmail,
|
||||
contact_phone: values.contactPhone,
|
||||
company_size: values.companySize,
|
||||
details: values.details,
|
||||
country: values.country,
|
||||
},
|
||||
])
|
||||
|
||||
// TODO: handle error
|
||||
console.log('error:', error)
|
||||
if (!error) {
|
||||
resetForm()
|
||||
|
||||
setFormSubmitted(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form initialValues={INITIAL_VALUES} validate={validate} onSubmit={handleFormSubmit}>
|
||||
{({ values, isSubmitting }: any) => {
|
||||
const selectedCountry = countries.find((country) => country.code === values['country'])
|
||||
const phonePlaceholder =
|
||||
selectedCountry !== undefined ? `+${selectedCountry.phone_code}` : ''
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>
|
||||
<Input
|
||||
label="Company Name *"
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
layout="vertical"
|
||||
placeholder="Supa Inc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="Your First Name *"
|
||||
id="contactFirstName"
|
||||
name="contactFirstName"
|
||||
layout="vertical"
|
||||
placeholder="Jane"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="Your Last Name *"
|
||||
id="contactLastName"
|
||||
name="contactLastName"
|
||||
layout="vertical"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
label="Your Business Email *"
|
||||
id="contactEmail"
|
||||
name="contactEmail"
|
||||
layout="vertical"
|
||||
placeholder="janedoe@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Listbox label="Company Size" id="companySize" name="companySize" layout="vertical">
|
||||
<Listbox.Option label="---" value="">
|
||||
{'---'}
|
||||
</Listbox.Option>
|
||||
<Listbox.Option label="1-10" value="1-10">
|
||||
1-10 Employees
|
||||
</Listbox.Option>
|
||||
<Listbox.Option label="11-50" value="11-50">
|
||||
11-50 Employees
|
||||
</Listbox.Option>
|
||||
<Listbox.Option label="51-200" value="51-200">
|
||||
51-200 Employees
|
||||
</Listbox.Option>
|
||||
<Listbox.Option label="201-500" value="201-500">
|
||||
201-500 Employees
|
||||
</Listbox.Option>
|
||||
<Listbox.Option label="501-1000" value="501-1000">
|
||||
501-1000 Employees
|
||||
</Listbox.Option>
|
||||
<Listbox.Option label="1001-5000" value="1001-5000">
|
||||
1001-5000 Employees
|
||||
</Listbox.Option>
|
||||
<Listbox.Option label="5001+" value="5001+">
|
||||
5001+ Employees
|
||||
</Listbox.Option>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
label="Your Phone Number"
|
||||
id="contactPhone"
|
||||
name="contactPhone"
|
||||
layout="vertical"
|
||||
placeholder={phonePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Listbox
|
||||
label="Country / Main Timezone"
|
||||
id="country"
|
||||
name="country"
|
||||
layout="vertical"
|
||||
>
|
||||
{countries.map(({ code, name }) => (
|
||||
<Listbox.Option label={name} key={code} value={code}>
|
||||
{name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<Input.TextArea
|
||||
id="details"
|
||||
name="details"
|
||||
label="Additional Details"
|
||||
placeholder="Tell us about your projects, clients, and technology..."
|
||||
rows={10}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex w-full flex-row-reverse pt-4">
|
||||
<Button
|
||||
size="xlarge"
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
htmlType="submit"
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
|
||||
{formSubmitted && (
|
||||
<h3 className="h3 mt-12 text-center">Thanks, we'll reach out to you shortly.</h3>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EnterpriseContactForm
|
||||
@@ -67,27 +67,48 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Day 4",
|
||||
"shipped": false,
|
||||
"title": "Supabase Wrappers - FDW Framework",
|
||||
"shipped": true,
|
||||
"date": "15 Dec 2022",
|
||||
"description": "Day 4",
|
||||
"description": "Supabase Wrappers",
|
||||
"d": 4,
|
||||
"dd": "Thursday",
|
||||
"youtube_id": "CGZr5tybW18",
|
||||
"blogpost": "https://supabase.com/blog",
|
||||
"youtube_id": "QA2qC5F-4OU",
|
||||
"blogpost": "/blog/postgres-foreign-data-wrappers-rust",
|
||||
"docs": "https://supabase.com/docs",
|
||||
"steps": []
|
||||
"steps": [
|
||||
{
|
||||
"title": "Supabase Wrappers",
|
||||
"blog": "/blog/postgres-foreign-data-wrappers-rust",
|
||||
"docs": "https://supabase.github.io/wrappers/",
|
||||
"isNew": true,
|
||||
"description": "Connect Postgres to external systems with Postgres Foreign Data Wrappers"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Day 5",
|
||||
"shipped": false,
|
||||
"title": "Supabase Vault Release",
|
||||
"shipped": true,
|
||||
"date": "16 Dec 2022",
|
||||
"description": "Day 5",
|
||||
"description": "Vault Release",
|
||||
"d": 5,
|
||||
"dd": "Friday",
|
||||
"youtube_id": "CGZr5tybW18",
|
||||
"youtube_id": "6bGQotxisoY",
|
||||
"blogpost": "https://supabase.com/blog",
|
||||
"docs": "https://supabase.com/docs",
|
||||
"steps": []
|
||||
"steps": [
|
||||
{
|
||||
"title": "Vault Release",
|
||||
"blog": "/blog/vault-now-in-beta",
|
||||
"isNew": true,
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"title": "Transparent Column Encryption",
|
||||
"description": "Faster asset delivery, now even faster.",
|
||||
"blog": "/blog/transparent-column-encryption-with-postgres",
|
||||
"isNew": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,206 +1,19 @@
|
||||
import { Button, Form, Input, InputNumber, Select } from 'ui'
|
||||
import { useState } from 'react'
|
||||
import countries from '~/data/Countries.json'
|
||||
import { Button } from 'ui'
|
||||
import { SupabaseClient } from '~/lib/supabase'
|
||||
|
||||
const INITIAL_VALUES = {
|
||||
type: 'expert',
|
||||
first: '',
|
||||
last: '',
|
||||
company: '',
|
||||
size: '',
|
||||
title: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
country: 'US',
|
||||
details: '',
|
||||
}
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
|
||||
if (!values.first) {
|
||||
errors.first = 'Required'
|
||||
}
|
||||
|
||||
if (!values.last) {
|
||||
errors.last = 'Required'
|
||||
}
|
||||
|
||||
if (!values.email) {
|
||||
errors.email = 'Required'
|
||||
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
|
||||
errors.email = 'Invalid email address'
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function BecomeAPartner({ supabase }: { supabase: SupabaseClient }) {
|
||||
const [formSubmitted, setFormSubmitted] = useState<boolean>(false)
|
||||
|
||||
const handleFormSubmit = async (values: any) => {
|
||||
const { error } = await supabase.from('partner_contacts').insert([
|
||||
{
|
||||
type: values.type,
|
||||
first: values.first,
|
||||
last: values.last,
|
||||
company: values.company,
|
||||
size: Number(values.size),
|
||||
title: values.title,
|
||||
email: values.email,
|
||||
website: values.email.split('@')[1],
|
||||
phone: values.phone,
|
||||
country: values.country,
|
||||
details: values.details,
|
||||
},
|
||||
])
|
||||
|
||||
// TODO: handle error
|
||||
console.log('error:', error)
|
||||
|
||||
setFormSubmitted(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-t">
|
||||
<div id="become-a-partner" className="mx-auto max-w-2xl space-y-12 py-12 px-6">
|
||||
<h2 className="h2">Become a Partner</h2>
|
||||
|
||||
<Form initialValues={INITIAL_VALUES} validate={validate} onSubmit={handleFormSubmit}>
|
||||
{({ isSubmitting }: any) => (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>
|
||||
<Select
|
||||
id="type"
|
||||
name="type"
|
||||
className="font-sans"
|
||||
label="What type of partner are you?"
|
||||
layout="vertical"
|
||||
>
|
||||
<Select.Option value="expert" selected={true}>
|
||||
Expert (Agency & Consulting)
|
||||
</Select.Option>
|
||||
<Select.Option value="technology">Technology</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 sm:space-x-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="First Name *"
|
||||
id="first"
|
||||
name="first"
|
||||
layout="vertical"
|
||||
placeholder="Jane"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="Last Name *"
|
||||
id="last"
|
||||
name="last"
|
||||
layout="vertical"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 sm:space-x-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="Company Name"
|
||||
id="company"
|
||||
name="company"
|
||||
layout="vertical"
|
||||
placeholder="Supa Inc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<InputNumber
|
||||
label="Company Size"
|
||||
id="size"
|
||||
name="size"
|
||||
layout="vertical"
|
||||
placeholder="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 sm:space-x-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="Job Title"
|
||||
id="title"
|
||||
name="title"
|
||||
layout="vertical"
|
||||
placeholder="CEO"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="Business email *"
|
||||
id="email"
|
||||
name="email"
|
||||
layout="vertical"
|
||||
placeholder="janedoe@example.sg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
label="Phone Number"
|
||||
id="phone"
|
||||
name="phone"
|
||||
layout="vertical"
|
||||
placeholder="+65 1234 1234"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Select
|
||||
label="Country / Main Timezone"
|
||||
id="country"
|
||||
name="country"
|
||||
layout="vertical"
|
||||
>
|
||||
{countries.map(({ code, name }: any, i: number) => (
|
||||
<Select.Option key={i} value={code}>
|
||||
{name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Input.TextArea
|
||||
id="details"
|
||||
name="details"
|
||||
label="Additional Details"
|
||||
placeholder="Tell us about your projects, clients, and technology..."
|
||||
rows={10}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex w-full flex-row-reverse pt-4">
|
||||
<Button
|
||||
size="xlarge"
|
||||
disabled={formSubmitted}
|
||||
loading={isSubmitting}
|
||||
htmlType="submit"
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
{formSubmitted && <h3 className="h3">Thanks, we'll reach out to you shortly 👁⚡️👁</h3>}
|
||||
<div id="become-a-partner" className="mx-auto max-w-2xl space-y-12 py-32 px-6 text-center">
|
||||
<h2 className="h2">Ready to work together?</h2>
|
||||
<Link href="https://forms.supabase.com/partner" as="https://forms.supabase.com/partner">
|
||||
<a>
|
||||
<Button size="medium" className="text-white">
|
||||
Become a partner
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -31,19 +31,15 @@ function GithubExamples() {
|
||||
Supported by a network of early advocates, contributors, and champions.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-2 py-4">
|
||||
<Link href="/docs/guides/examples#guides" as="/docs/guides/examples#guides">
|
||||
<Link href="/docs/guides/auth/overview" passHref>
|
||||
<Button size="small" as="a" type="default" icon={<IconBookOpen size={12} />}>
|
||||
View guides
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/docs/guides/examples" as="/docs/guides/examples">
|
||||
<Button size="small" as="a" type="default">
|
||||
View all examples
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/supabase/supabase/tree/master/examples"
|
||||
as="https://github.com/supabase/supabase/tree/master/examples"
|
||||
passHref
|
||||
>
|
||||
<Button size="small" as="a" type="default" icon={<IconGitHub size={12} />}>
|
||||
Official GitHub library
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
},
|
||||
{
|
||||
"text": "SOC2",
|
||||
"url": "/legal/soc2"
|
||||
"url": "https://forms.supabase.com/soc2"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"author_id": "angelico_de_los_reyes",
|
||||
"author": "Angelico de los Reyes",
|
||||
"position": "",
|
||||
"position": "Engineering",
|
||||
"author_url": "https://github.com/dragarcia",
|
||||
"author_image_url": "https://github.com/dragarcia.png"
|
||||
},
|
||||
@@ -251,6 +251,13 @@
|
||||
"author_url": "https://github.com/fenos",
|
||||
"author_image_url": "https://github.com/fenos.png"
|
||||
},
|
||||
{
|
||||
"author_id": "bo_lu",
|
||||
"author": "Bo Lu",
|
||||
"position": "Engineering",
|
||||
"author_url": "https://github.com/burmecia",
|
||||
"author_image_url": "https://github.com/burmecia.png"
|
||||
},
|
||||
{
|
||||
"author_id": "stojan",
|
||||
"author": "Stojan Dimitrovski",
|
||||
|
||||
@@ -5,6 +5,7 @@ import ImageGrid from '~/components/ImageGrid'
|
||||
import Quote from '~/components/Quote'
|
||||
import Chart from '~/components/Charts/PGCharts'
|
||||
import InlineCodeTag from '~/components/InlineCode'
|
||||
import { Badge } from 'ui'
|
||||
|
||||
// import all components used in blog articles here
|
||||
// to do: move this into a helper/utils, it is used elsewhere
|
||||
@@ -14,6 +15,7 @@ const ignoreClass = 'ignore-on-export'
|
||||
export default function mdxComponents(type?: 'blog' | 'lp' | undefined) {
|
||||
const components = {
|
||||
CodeBlock,
|
||||
Badge,
|
||||
Quote,
|
||||
Avatar,
|
||||
PGChart: (props: any) => {
|
||||
|
||||
@@ -1619,88 +1619,93 @@ module.exports = [
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth-twilio',
|
||||
source: '/docs/guides/auth/auth-twilio',
|
||||
destination: '/docs/guides/auth/phone-login/twilio',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-google',
|
||||
destination: '/docs/guides/auth/social-login/with-google',
|
||||
source: '/docs/guides/auth/auth-vonage',
|
||||
destination: '/docs/guides/auth/phone-login/vonage',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-facebook',
|
||||
destination: '/docs/guides/auth/social-login/with-facebook',
|
||||
source: '/docs/guides/auth/auth-google',
|
||||
destination: '/docs/guides/auth/social-login/auth-google',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-apple',
|
||||
destination: '/docs/guides/auth/social-login/with-apple',
|
||||
source: '/docs/guides/auth/auth-facebook',
|
||||
destination: '/docs/guides/auth/social-login/auth-facebook',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-azure',
|
||||
destination: '/docs/guides/auth/social-login/with-azure',
|
||||
source: '/docs/guides/auth/auth-apple',
|
||||
destination: '/docs/guides/auth/social-login/auth-apple',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-twitter',
|
||||
destination: '/docs/guides/auth/social-login/with-twitter',
|
||||
source: '/docs/guides/auth/auth-azure',
|
||||
destination: '/docs/guides/auth/social-login/auth-azure',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-github',
|
||||
destination: '/docs/guides/auth/social-login/with-github',
|
||||
source: '/docs/guides/auth/auth-twitter',
|
||||
destination: '/docs/guides/auth/social-login/auth-twitter',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-gitlab',
|
||||
destination: '/docs/guides/auth/social-login/with-gitlab',
|
||||
source: '/docs/guides/auth/auth-github',
|
||||
destination: '/docs/guides/auth/social-login/auth-github',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-bitbucket',
|
||||
destination: '/docs/guides/auth/social-login/with-bitbucket',
|
||||
source: '/docs/guides/auth/auth-gitlab',
|
||||
destination: '/docs/guides/auth/social-login/auth-gitlab',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-discord',
|
||||
destination: '/docs/guides/auth/social-login/with-discord',
|
||||
source: '/docs/guides/auth/auth-bitbucket',
|
||||
destination: '/docs/guides/auth/social-login/auth-bitbucket',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-keycloak',
|
||||
destination: '/docs/guides/auth/social-login/with-keycloak',
|
||||
source: '/docs/guides/auth/auth-discord',
|
||||
destination: '/docs/guides/auth/social-login/auth-discord',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-linkedin',
|
||||
destination: '/docs/guides/auth/social-login/with-linkedin',
|
||||
source: '/docs/guides/auth/auth-keycloak',
|
||||
destination: '/docs/guides/auth/social-login/auth-keycloak',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-notion',
|
||||
destination: '/docs/guides/auth/social-login/with-notion',
|
||||
source: '/docs/guides/auth/auth-linkedin',
|
||||
destination: '/docs/guides/auth/social-login/auth-linkedin',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-slack',
|
||||
destination: '/docs/guides/auth/social-login/with-slack',
|
||||
source: '/docs/guides/auth/auth-notion',
|
||||
destination: '/docs/guides/auth/social-login/auth-notion',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-spotify',
|
||||
destination: '/docs/guides/auth/social-login/with-spotify',
|
||||
source: '/docs/guides/auth/auth-slack',
|
||||
destination: '/docs/guides/auth/social-login/auth-slack',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-twitch',
|
||||
destination: '/docs/guides/auth/social-login/with-twitch',
|
||||
source: '/docs/guides/auth/auth-spotify',
|
||||
destination: '/docs/guides/auth/social-login/auth-spotify',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/with-workos',
|
||||
destination: '/docs/guides/auth/social-login/with-workos',
|
||||
source: '/docs/guides/auth/auth-twitch',
|
||||
destination: '/docs/guides/auth/social-login/auth-twitch',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/auth/auth-workos',
|
||||
destination: '/docs/guides/auth/social-login/auth-workos',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
@@ -1877,4 +1882,14 @@ module.exports = [
|
||||
source: '/docs/handbook/supasquad',
|
||||
destination: '/supasquad',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/contact/enterprise',
|
||||
destination: 'https://forms.supabase.com/enterprise',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/legal/soc2',
|
||||
destination: 'https://forms.supabase.com/soc2',
|
||||
},
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { useRouter } from 'next/router'
|
||||
import EnterpriseContactForm from '~/components/EnterpriseContactForm'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
|
||||
const meta_title = 'Enterprise Contact | Supabase'
|
||||
const meta_description = 'Contact Supabase for enterprise sales and pricing.'
|
||||
|
||||
const EnterpriseContactPage = () => {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<NextSeo
|
||||
title={meta_title}
|
||||
description={meta_description}
|
||||
openGraph={{
|
||||
title: meta_title,
|
||||
description: meta_description,
|
||||
url: `https://supabase.com/${router.pathname}`,
|
||||
images: [
|
||||
{
|
||||
url: `https://supabase.com/images/og/og-image.jpg`,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mx-auto my-16 max-w-xl px-4 md:my-20 lg:my-24 xl:my-32">
|
||||
<h2 className="h2 block pb-16 text-center">Contact Enterprise Sales</h2>
|
||||
|
||||
<EnterpriseContactForm />
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default EnterpriseContactPage
|
||||
@@ -38,7 +38,7 @@ export default function launchweek() {
|
||||
const { isDarkMode } = useTheme()
|
||||
const title = 'Launch Week 6'
|
||||
const description = 'Supabase Launch Week 6 | 12-18 Dec 2022'
|
||||
const liveDay = 'Wednesday'
|
||||
const liveDay = 'Friday'
|
||||
|
||||
const [supabase] = useState(() =>
|
||||
createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)
|
||||
@@ -130,14 +130,16 @@ export default function launchweek() {
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href={docs} target="_blank" rel="noopener">
|
||||
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
|
||||
Docs
|
||||
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
|
||||
<img src="/images/launchweek/icons-docs.svg" className="w-4 h-4"></img>
|
||||
{docs && (
|
||||
<a href={docs} target="_blank" rel="noopener">
|
||||
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
|
||||
Docs
|
||||
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
|
||||
<img src="/images/launchweek/icons-docs.svg" className="w-4 h-4"></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
)}
|
||||
{video && (
|
||||
<a href={video} target="_blank" rel="noopener">
|
||||
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
|
||||
@@ -217,18 +219,23 @@ export default function launchweek() {
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
<a className="flex items-center justify-center mb-20" href="#currentDay">
|
||||
<div className="border border-slate-600 text-black dark:text-white text-sm rounded-full bg-slate-400 px-2 py-1 hover:bg-slate-600">
|
||||
Jump to current day
|
||||
</div>
|
||||
</a>
|
||||
{process.env.NEXT_PUBLIC_LW_STARTED && (
|
||||
<>
|
||||
<SectionContainer className="!pt-0 ">
|
||||
<SectionContainer className="!py-0 ">
|
||||
<div className="border rounded-2xl border-slate-400 text-sm px-5 py-4 flex flex-col sm:flex-row justify-between items-center">
|
||||
<div className="relative flex items-center mb-4 sm:mb-0">
|
||||
<div className="flex">
|
||||
<Image
|
||||
src="/images/launchweek/antcopplecall.png"
|
||||
className="rounded-xl w-32 h-32 brightness-125"
|
||||
<div className="flex min-w-[150px]">
|
||||
<img
|
||||
src={`/images/launchweek/antcopplecall.png`}
|
||||
className="brightness-125"
|
||||
width={120}
|
||||
height={80}
|
||||
></Image>
|
||||
></img>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row ml-8 sm:ml-10">
|
||||
<span className="text-black dark:text-white mr-2">Who we hire at Supabase</span>
|
||||
@@ -248,7 +255,34 @@ export default function launchweek() {
|
||||
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2 min-w-[142px]">
|
||||
Read blogpost
|
||||
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
|
||||
<img src="/images/launchweek/blog-icon.svg" className="w-3 h-3"></img>
|
||||
<img src="/images/launchweek/icons-blogpost.svg" className="w-3 h-3"></img>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
<SectionContainer className="!pt-4 ">
|
||||
<div className="border rounded-2xl border-slate-400 text-sm px-5 py-4 flex flex-col sm:flex-row justify-between items-center">
|
||||
<div className="relative flex items-center mb-4 sm:mb-0">
|
||||
<div className="flex min-w-[150px]">
|
||||
<img
|
||||
src={`/images/launchweek/${isDarkMode ? 'outro.svg' : 'outro-light.svg'}`}
|
||||
width={110}
|
||||
height={80}
|
||||
></img>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row ml-8 sm:ml-10">
|
||||
<span className="text-black dark:text-white mr-2">Wrap Up</span>
|
||||
<span className="text-slate-900">Everything we shipped</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 z-10">
|
||||
<a href={'/blog/launch-week-6-wrap-up'} target="_blank" rel="noopener">
|
||||
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2 min-w-[142px]">
|
||||
Read blogpost
|
||||
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
|
||||
<img src="/images/launchweek/icons-blogpost.svg" className="w-3 h-3"></img>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -264,7 +298,13 @@ export default function launchweek() {
|
||||
justified={false}
|
||||
bordered={false}
|
||||
chevronAlign="right"
|
||||
defaultValue={[day1.d.toString(), day2.d.toString(), day3.d.toString()]}
|
||||
defaultValue={[
|
||||
day1.d.toString(),
|
||||
day2.d.toString(),
|
||||
day3.d.toString(),
|
||||
day4.d?.toString(),
|
||||
day5.d?.toString(),
|
||||
]}
|
||||
>
|
||||
<div className="border-b pb-3">
|
||||
<Accordion.Item
|
||||
@@ -291,7 +331,7 @@ export default function launchweek() {
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className={`flex flex-col flex-1 items-center gap-5 lg:items-start lg:justify-between border rounded-xl h-full relative overflow-hidden after:opacity- after:absolute after:bg-no-repeat after:bg-[center_bottom] lg:after:bg-[right_15%_top_100px] xl:after:bg-[right_15%_top_60px] after:bg-[length:300px_180px] after:lg:bg-[length:450px_300px] after:xl:bg-[length:528px_367px] after:bg-[url('/images/launchweek/docs-update-bg.png')] after:top-0 after:right-0 after:bottom-0 after:left-0 p-14 text-2xl before:absolute before:w-full before:h-full before:top-52 before:right-0 before:bottom-0 before:left-0 before:border-[#1f3536] before:-z-10 !px-3 sm:!px-14`}
|
||||
className={`flex flex-col flex-1 items-center gap-5 lg:items-start lg:justify-between border rounded-xl h-full relative overflow-hidden after:opacity- after:absolute after:bg-no-repeat after:bg-[center_bottom] lg:after:bg-[right_15%_top_100px] xl:after:bg-[right_15%_top_60px] after:bg-[length:300px_180px] after:lg:bg-[length:450px_300px] after:xl:bg-[length:528px_367px] after:dark:bg-[url('/images/launchweek/docs-update-bg.png')] after:bg-[url('/images/launchweek/docs-update-bg-light.png')] after:top-0 after:right-0 after:bottom-0 after:left-0 p-14 text-2xl before:absolute before:w-full before:h-full before:top-52 before:right-0 before:bottom-0 before:left-0 before:border-[#1f3536] before:-z-10 !px-3 sm:!px-14`}
|
||||
>
|
||||
<div className="flex items-center relative z-10 justify-between flex-col-reverse lg:flex-row lg:justify-start gap-2 text-black dark:text-white">
|
||||
<span>{day1.description}</span>
|
||||
@@ -389,7 +429,7 @@ export default function launchweek() {
|
||||
{day3.steps.length > 0 && (
|
||||
<div className="h-[400px] flex gap-5 group">
|
||||
<div
|
||||
className={`flex flex-col text-center items-center lg:items-start justify-between flex-1 border rounded-xl h-full bg-no-repeat p-14 text-2xl relative`} // bg-[url('/images/launchweek/mfa-bg-light.png')] dark:bg-[url('/images/launchweek/mfa-bg.png')] bg-[bottom_right_30%] bg-contain
|
||||
className={`flex flex-col text-center items-center lg:items-start justify-between flex-1 border rounded-xl h-full bg-no-repeat p-14 text-2xl relative`}
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-full h-full -z-20 ">
|
||||
<Image
|
||||
@@ -444,35 +484,58 @@ export default function launchweek() {
|
||||
id={day4.d.toString()}
|
||||
>
|
||||
{day4.steps.length > 0 && (
|
||||
<div className="h-[800px] lg:h-[400px] flex flex-col gap-5 lg:flex-row">
|
||||
<div className="h-[400px] flex flex-col gap-5 lg:flex-row group/day4 relative overflow-hidden">
|
||||
<div
|
||||
className={`flex flex-col justify-between flex-1 basis-1/2 lg:basis-2/3 border rounded-xl h-full p-14 text-2xl bg-no-repeat bg-contain bg-[url('/images/launchweek/vault-bg-light.png')] dark:bg-[url('/images/launchweek/vault-bg.png')] bg-[center_bottom]`}
|
||||
className={`relative flex flex-col items-center justify-between lg:items-start flex-1 basis-1/2 lg:basis-2/3 border rounded-xl h-full p-14 text-2xl bg-no-repeat bg-cover !px-3 sm:!px-14`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-20 ${styles.wrappers}`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/wrappers-visual.svg'
|
||||
: '/images/launchweek/wrappers-visual-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
priority
|
||||
className="left-16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-10 opacity-0 group-hover/day4:opacity-100 duration-500 transition-all ${styles.wrappers}`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/wrappers-visual-hover.svg'
|
||||
: '/images/launchweek/wrappers-visual-hover-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
className="test"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center flex-col-reverse lg:flex-row text-black dark:text-white">
|
||||
<span>{day4.steps[0].title}</span>
|
||||
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#598973] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#598973]">
|
||||
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
|
||||
New
|
||||
</Badge>
|
||||
</div>
|
||||
<SectionButtons docs={day4.steps[0].docs} blog={day4.steps[0].blog} />
|
||||
</div>
|
||||
<div
|
||||
className={`flex-1 flex flex-col items-center justify-between basis-1/2 lg:basis-1/3 border rounded-xl h-full bg-no-repeat p-14 text-2xl bg-[url('/images/launchweek/encryption-bg-light.png')] dark:bg-[url('/images/launchweek/encryption-bg.svg')] bg-contain bg-[center_center] shadow-[inset_0px_130px_50px_-52p_rgb(10,31,30)]`}
|
||||
>
|
||||
{/* inset 0px 130px 50px -52px #121f1e; */}
|
||||
<div className="flex flex-col items-center gap-2 min-w-[300px]">
|
||||
<Badge className="!bg-transparent h-fit text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#598973] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#598973]">
|
||||
New
|
||||
</Badge>
|
||||
<span className="text-center">{day4.steps[1].title}</span>
|
||||
</div>
|
||||
<SectionButtons docs={day4.steps[1].docs} blog={day4.steps[1].blog} />
|
||||
<SectionButtons
|
||||
docs={day4.steps[0].docs}
|
||||
blog={day4.steps[0].blog}
|
||||
video={`https://www.youtube.com/watch?v=${day4.youtube_id}&ab_channel=Supabase`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Accordion.Item>
|
||||
</div>
|
||||
<div className="border-b pb-3">
|
||||
<div className="border-b pb-3" id="currentDay">
|
||||
<Accordion.Item
|
||||
header={
|
||||
<AccordionHeader
|
||||
@@ -487,30 +550,478 @@ export default function launchweek() {
|
||||
id={day5.d.toString()}
|
||||
>
|
||||
{day5.steps.length > 0 && (
|
||||
<div className="h-[800px] lg:h-[400px] flex flex-col gap-5 lg:flex-row">
|
||||
<div
|
||||
className={`flex flex-col justify-between flex-1 basis-1/2 lg:basis-2/3 border rounded-xl h-full bg-no-repeat p-14 text-2xl bg-[url('/images/launchweek/wrappers-bg-light.png')] dark:bg-[url('/images/launchweek/wrappers-bg.png')] bg-contain bg-[right]`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>{day5.steps[0].title}</span>
|
||||
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#598973] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#598973]">
|
||||
New
|
||||
</Badge>
|
||||
<>
|
||||
<div className="h-[800px] lg:h-[400px] flex flex-col gap-5 lg:flex-row">
|
||||
<div
|
||||
className={`relative group/day5step1 flex flex-col items-center justify-between lg:items-start flex-1 basis-1/2 lg:basis-2/3 border rounded-xl h-full bg-no-repeat p-14 text-2xl overflow-hidden`}
|
||||
>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-20 ${styles.wrappers}`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/vault-visual.svg'
|
||||
: '/images/launchweek/vault-visual-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
priority
|
||||
className="left-16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-10 opacity-0 group-hover/day5step1:opacity-100 duration-500 transition-all ${styles.wrappers}`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/vault-visual-hover.svg'
|
||||
: '/images/launchweek/vault-visual-hover-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
className="test"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center flex-col-reverse lg:flex-row">
|
||||
<span className="text-black dark:text-white">
|
||||
{day5.steps[0].title}
|
||||
</span>
|
||||
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
|
||||
New
|
||||
</Badge>
|
||||
</div>
|
||||
<SectionButtons docs={day5.steps[0].docs} blog={day5.steps[0].blog} />
|
||||
</div>
|
||||
<SectionButtons docs={day5.steps[0].docs} blog={day5.steps[0].blog} />
|
||||
</div>
|
||||
<div
|
||||
className={`flex-1 flex flex-col items-center justify-between basis-1/2 lg:basis-1/3 border rounded-xl h-full bg-no-repeat p-14 text-2xl bg-[url('/images/launchweek/pitr-bg-light.png')] dark:bg-[url('/images/launchweek/pitr-bg.png')] bg-cover bg-[center_center]`}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2 min-w-[300px]">
|
||||
<Badge className="!bg-transparent h-fit text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#598973] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#598973]">
|
||||
New
|
||||
</Badge>
|
||||
<span>{day5.steps[1].title}</span>
|
||||
<div
|
||||
className={`relative group/day5step2 flex-1 flex flex-col items-center justify-between basis-1/2 lg:basis-1/3 border rounded-xl h-full bg-no-repeat p-14 text-2xl overflow-hidden`}
|
||||
>
|
||||
<div className={`absolute top-0 right-0 w-full h-full -z-20`}>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/TCE-visual.svg'
|
||||
: '/images/launchweek/TCE-visual-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
priority
|
||||
className="left-16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-10 opacity-0 group-hover/day5step2:opacity-100 duration-500 transition-all`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/TCE-visual-hover.svg'
|
||||
: '/images/launchweek/TCE-visual-hover-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
className="test"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 min-w-[300px]">
|
||||
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
|
||||
New
|
||||
</Badge>
|
||||
<span className="text-black dark:text-white text-center">
|
||||
{day5.steps[1].title}
|
||||
</span>
|
||||
</div>
|
||||
<SectionButtons docs={day5.steps[1].docs} blog={day5.steps[1].blog} />
|
||||
</div>
|
||||
<SectionButtons docs={day5.steps[1].docs} blog={day5.steps[1].blog} />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-black dark:text-white text-lg mb-4 mt-4">Community</h3>
|
||||
<div className="h-[400px] flex flex-col gap-5 lg:flex-row group/community relative overflow-hidden">
|
||||
<div
|
||||
className={`relative flex flex-col items-center justify-between lg:items-start flex-1 basis-1/2 lg:basis-2/3 border rounded-xl h-full p-14 text-2xl bg-no-repeat bg-cover !px-3 sm:!px-14`}
|
||||
>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-20 ${styles.community_wrappers}`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/community-visual.svg'
|
||||
: '/images/launchweek/community-visual-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
priority
|
||||
className="left-16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-10 opacity-0 group-hover/community:opacity-100 duration-500 transition-all ${styles.community_wrappers}`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/community-visual-hover.svg'
|
||||
: '/images/launchweek/community-visual-light-hover.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
className="test"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center flex-col-reverse lg:flex-row">
|
||||
<span className="text-black dark:text-white">Community Day</span>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row gap-4 md:gap-2 z-10 ">
|
||||
<div className="flex gap-4 md:gap-2">
|
||||
<a
|
||||
href={'/blog/launch-week-6-community-day'}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
|
||||
Blog post
|
||||
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
|
||||
<img
|
||||
src="/images/launchweek/icons-blogpost.svg"
|
||||
className="w-4 h-4"
|
||||
></img>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={'https://www.youtube.com/watch?v=hw9Q-NjASbU'}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
|
||||
Flutterflow
|
||||
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
|
||||
<img
|
||||
src="/images/launchweek/video-icon.svg"
|
||||
className="w-4 h-4"
|
||||
></img>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex gap-4 md:gap-2">
|
||||
<a
|
||||
href={'https://www.youtube.com/watch?v=mw0DLwItue4'}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
|
||||
OneSignal
|
||||
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
|
||||
<img
|
||||
src="/images/launchweek/video-icon.svg"
|
||||
className="w-4 h-4"
|
||||
></img>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={'https://www.youtube.com/watch?v=EdYQ9fF-hz4'}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<div className="flex items-center border border-slate-400 bg-gradient-to-r from-[#fcfcfc] to-[#f2f2f2] hover:to-[#d5d5d5] text-black dark:text-white dark:from-[#191919] dark:to-[#464444] dark:hover:to-[#4e4e4e] rounded-full text-sm py-2 pl-3 pr-2">
|
||||
NextAuth
|
||||
<div className="bg-[#eeeeee] dark:bg-[#313131] rounded-full inline-block p-1 ml-2">
|
||||
<img
|
||||
src="/images/launchweek/video-icon.svg"
|
||||
className="w-4 h-4"
|
||||
></img>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-black dark:text-white text-lg mb-4 mt-4">
|
||||
One more thing
|
||||
</h3>
|
||||
<div className="flex flex-col lg:grid grid-cols-3 grid-rows-2 gap-4">
|
||||
<div
|
||||
className={`min-h-[400px] relative group/day5step2 flex-1 flex flex-col items-center justify-between basis-1/2 lg:basis-1/3 border rounded-xl bg-no-repeat p-14 text-2xl overflow-hidden`}
|
||||
>
|
||||
<div className={`absolute top-0 right-0 w-full h-full -z-20`}>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/PgGraphql-visual.svg'
|
||||
: '/images/launchweek/PgGraphql-visual-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
priority
|
||||
className="left-16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-10 opacity-0 group-hover/day5step2:opacity-100 duration-500 transition-all`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/PgGraphql-visual-hover.svg'
|
||||
: '/images/launchweek/PgGraphql-visual-hover-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
className="test"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 min-w-[300px]">
|
||||
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
|
||||
Updated
|
||||
</Badge>
|
||||
<span className="text-black dark:text-white text-center">
|
||||
pg_graphql v1.0
|
||||
</span>
|
||||
</div>
|
||||
<SectionButtons
|
||||
docs="/docs/guides/api#graphql-api"
|
||||
blog="/blog/pg-graphql-v1"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`min-h-[400px] relative group/day5step2 flex-1 flex flex-col items-center justify-between basis-1/2 lg:basis-1/3 border rounded-xl bg-no-repeat p-14 text-2xl overflow-hidden`}
|
||||
>
|
||||
<div className={`absolute top-0 right-0 w-full h-full -z-20`}>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/custom-domains-visual.svg'
|
||||
: '/images/launchweek/custom-domains-visual-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
priority
|
||||
className="left-16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-10 opacity-0 group-hover/day5step2:opacity-100 duration-500 transition-all`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/custom-domains-visual-hover.svg'
|
||||
: '/images/launchweek/custom-domains-visual-hover-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
className="test"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 min-w-[300px]">
|
||||
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
|
||||
New
|
||||
</Badge>
|
||||
<span className="text-black dark:text-white text-center">
|
||||
Custom Domains
|
||||
</span>
|
||||
</div>
|
||||
<SectionButtons
|
||||
docs="/docs/guides/platform/custom-domains"
|
||||
blog="/blog/custom-domain-names"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`min-h-[400px] relative group/day5step2 flex-1 flex flex-col items-center justify-between basis-1/2 lg:basis-1/3 border rounded-xl bg-no-repeat p-14 text-2xl overflow-hidden`}
|
||||
>
|
||||
<div className={`absolute top-0 right-0 w-full h-full -z-20`}>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/PITR-visual.svg'
|
||||
: '/images/launchweek/PITR-visual-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
priority
|
||||
className="left-16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-10 opacity-0 group-hover/day5step2:opacity-100 duration-500 transition-all`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/PITR-visual-hover.svg'
|
||||
: '/images/launchweek/PITR-visual-hover-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
className="test"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 min-w-[300px]">
|
||||
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
|
||||
New
|
||||
</Badge>
|
||||
<span className="text-black dark:text-white text-center">
|
||||
Point-in-time recovery
|
||||
</span>
|
||||
</div>
|
||||
<SectionButtons
|
||||
docs="/docs/guides/platform/going-into-prod"
|
||||
blog="/blog/postgres-point-in-time-recovery"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`min-h-[400px] relative group/day5step2 flex-1 flex flex-col items-center justify-between basis-1/2 lg:basis-1/3 border rounded-xl bg-no-repeat p-14 text-2xl overflow-hidden`}
|
||||
>
|
||||
<div className={`absolute top-0 right-0 w-full h-full -z-20`}>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/pg_crdt-visual.svg'
|
||||
: '/images/launchweek/pg_crdt-visual-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
priority
|
||||
className="left-16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-10 opacity-0 group-hover/day5step2:opacity-100 duration-500 transition-all`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/pg_crdt-visual-hover.svg'
|
||||
: '/images/launchweek/pg_crdt-visual-hover-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
className="test"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 min-w-[300px]">
|
||||
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
|
||||
Experimental
|
||||
</Badge>
|
||||
<span className="text-black dark:text-white text-center">
|
||||
pg_crdt
|
||||
</span>
|
||||
</div>
|
||||
<SectionButtons
|
||||
docs="https://github.com/supabase/pg_crdt"
|
||||
blog="/blog/postgres-crdt"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`min-h-[400px] relative group/day5step2 flex-1 flex flex-col items-center justify-between basis-1/2 lg:basis-1/3 border rounded-xl bg-no-repeat p-14 text-2xl overflow-hidden`}
|
||||
>
|
||||
<div className={`absolute top-0 right-0 w-full h-full -z-20`}>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/postgres-visual.svg'
|
||||
: '/images/launchweek/postgres-visual-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
priority
|
||||
className="left-16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-10 opacity-0 group-hover/day5step2:opacity-100 duration-500 transition-all`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/postgres-visual-hover.svg'
|
||||
: '/images/launchweek/postgres-visual-hover-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
className="test"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 min-w-[300px]">
|
||||
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
|
||||
Upgrade
|
||||
</Badge>
|
||||
<span className="text-black dark:text-white text-center">
|
||||
Postgres 15
|
||||
</span>
|
||||
</div>
|
||||
<SectionButtons
|
||||
docs="https://www.postgresql.org/docs/15/release-15.html"
|
||||
blog="/blog/new-in-postgres-15"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`min-h-[400px] relative group/day5step2 flex-1 flex flex-col items-center justify-between basis-1/2 lg:basis-1/3 border rounded-xl bg-no-repeat p-14 text-2xl overflow-hidden`}
|
||||
>
|
||||
<div className={`absolute top-0 right-0 w-full h-full -z-20`}>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/PostgREST11-visual.svg'
|
||||
: '/images/launchweek/PostgREST11-visual-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
priority
|
||||
className="left-16"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute top-0 right-0 w-full h-full -z-10 opacity-0 group-hover/day5step2:opacity-100 duration-500 transition-all`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
isDarkMode
|
||||
? '/images/launchweek/PostgREST11-visual-hover.svg'
|
||||
: '/images/launchweek/PostgREST11-visual-hover-light.svg'
|
||||
}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
className="test"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 min-w-[300px]">
|
||||
<Badge className="!bg-transparent h-fit ml-4 text-sm !py-1 !px-4 text-transparent bg-clip-text bg-gradient-to-r from-[#99bbab] to-[#396f55] border-[#4d898c] dark:from-white dark:to-[#1a7a4ca1] dark:border-[#DFFFF1]">
|
||||
Upgrade
|
||||
</Badge>
|
||||
<span className="text-black dark:text-white text-center">
|
||||
PostgREST 11
|
||||
</span>
|
||||
</div>
|
||||
<SectionButtons
|
||||
docs="/docs/guides/resources/supabase-cli"
|
||||
blog="/blog/postgrest-11-prerelease"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Accordion.Item>
|
||||
</div>
|
||||
@@ -518,6 +1029,9 @@ export default function launchweek() {
|
||||
</SectionContainer>
|
||||
</>
|
||||
)}
|
||||
<SectionContainer className="!py-20 sm:!pb-40 sm:!pt-10"></SectionContainer>
|
||||
|
||||
<SectionContainer className="!pt-0 sm:!pb-20 !pb-40"></SectionContainer>
|
||||
<SectionContainer className="!pt-0 sm:!pb-20 !pb-40">
|
||||
<div className="flex flex-col mb-16">
|
||||
<Badge className="mb-4 w-fit bg-gradient-to-r from-[#0E3737C2] to-[#67947F] hover:to-[#39617D94] dark:hover:to-[#A6FFD899] text-whiteA-1200 dark:text-black font-normal !py-1 !px-4 dark:from-white dark:via-white dark:to-[#1a7a4c75] bg-slate-1200">
|
||||
|
||||
@@ -9,3 +9,17 @@
|
||||
.community {
|
||||
background-image: radial-gradient(closest-side at 50% 50%, #d9eeef, #dbeef0, white);
|
||||
}
|
||||
|
||||
.wrappers > span {
|
||||
left: -200px !important;
|
||||
}
|
||||
|
||||
.community_wrappers > span {
|
||||
left: -400px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.community_wrappers > span > img {
|
||||
object-fit: contain !important;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import CTABanner from 'components/CTABanner/index'
|
||||
import Layout from '~/components/Layouts/Default'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import SOC2RequestForm from '~/components/SOC2RequestForm'
|
||||
|
||||
const SOC2 = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<SectionContainer>
|
||||
<div className="mx-auto grid max-w-2xl grid-cols-12 rounded-lg">
|
||||
<div className="col-span-12 flex items-center lg:col-span-12">
|
||||
<div className="prose flex flex-col space-y-8 p-16">
|
||||
<h1 className="text-center text-5xl">SOC2</h1>
|
||||
<p>
|
||||
As a database company, being SOC2 compliant is important when handling sensitive
|
||||
customer data. Supabase is currently SOC2 Type 1 compliant and we're also working on
|
||||
getting certified for SOC2 Type 2 and HIPAA next.
|
||||
</p>
|
||||
|
||||
<p>You can request for our latest SOC 2 document by submitting your request here.</p>
|
||||
|
||||
<SOC2RequestForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
<CTABanner />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
export default SOC2
|
||||
@@ -76,7 +76,7 @@ export default function IndexPage() {
|
||||
},
|
||||
{
|
||||
name: 'Enterprise',
|
||||
href: '/contact/enterprise',
|
||||
href: 'https://forms.supabase.com/enterprise',
|
||||
description: 'For large-scale applications managing serious workloads.',
|
||||
features: [
|
||||
`Point in time recovery`,
|
||||
@@ -575,7 +575,7 @@ export default function IndexPage() {
|
||||
Designated support team, account manager and technical specialist
|
||||
</p>
|
||||
|
||||
<Link href="/contact/enterprise">
|
||||
<Link href="https://forms.supabase.com/enterprise">
|
||||
<a>
|
||||
<Button size="medium" type="default">
|
||||
Contact Us
|
||||
@@ -643,7 +643,7 @@ export default function IndexPage() {
|
||||
</td>
|
||||
|
||||
<td className="px-6 pt-5">
|
||||
<Link href="/contact/enterprise">
|
||||
<Link href="https://forms.supabase.com/enterprise">
|
||||
<a>
|
||||
<Button size="medium" type="default" block>
|
||||
Contact us
|
||||
|
||||
@@ -52,7 +52,7 @@ export const Section = ({ children, icon, img }) => (
|
||||
|
||||
### SOC 2
|
||||
|
||||
Supabase is SOC2 Type 1 compliant. Enterprise customers can request a copy of our SOC2 [here](https://supabase.com/legal/soc2).
|
||||
Supabase is SOC2 Type 1 compliant. Enterprise customers can request a copy of our SOC2 [here](https://forms.supabase.com/soc2).
|
||||
|
||||
<div className="w-16">
|
||||
<img src="/images/security/21972-312_SOC_NonCPA.png" />
|
||||
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 1002 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 167 KiB |