diff --git a/README.md b/README.md index da83a9e1e4e..70298a09e54 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i - [Nepali / नेपाली](/i18n/README.ne.md) - [Indonesian / Bahasa Indonesia](/i18n/README.id.md) - [Italiano / Italian](/i18n/README.it.md) -- [Japanese / 日本語](/i18n/README.ja.md) +- [Japanese / 日本語](/i18n/README.jp.md) - [Korean / 한국어](/i18n/README.ko.md) - [Lithuanian / lietuvių](/i18n/README.lt.md) - [Latvian / latviski](/i18n/README.lv.md) diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts index 844724a5466..337f48b2bbf 100644 --- a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts @@ -89,13 +89,13 @@ export const highlightSelectedNavItem = (id: string) => { const navMenuItems = document.querySelectorAll('.function-link-item a') // find any currently active items and remove them - const currentActiveItems = document.querySelectorAll('.function-link-list .text-brand-900') - currentActiveItems.forEach((item) => item.classList.remove('text-brand-900')) + const currentActiveItems = document.querySelectorAll('.function-link-list .text-brand') + currentActiveItems.forEach((item) => item.classList.remove('text-brand')) // Add active class to the current item navMenuItems.forEach((item) => { if (item.href.split('/').at(-1) === id) { - item.classList.add('text-brand-900') + item.classList.add('text-brand') } }) } diff --git a/apps/docs/components/GuidesTableOfContents.tsx b/apps/docs/components/GuidesTableOfContents.tsx index 9d25121f661..553f3165452 100644 --- a/apps/docs/components/GuidesTableOfContents.tsx +++ b/apps/docs/components/GuidesTableOfContents.tsx @@ -46,7 +46,7 @@ const GuidesTableOfContents: FC = ({ list }) => {
  • diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx index e760c411dc3..657083befd2 100644 --- a/apps/docs/components/HomePageCover.tsx +++ b/apps/docs/components/HomePageCover.tsx @@ -28,6 +28,11 @@ const HomePageCover = (props) => { icon: '/docs/img/icons/flutter-icon', href: '/guides/getting-started/quickstarts/flutter', }, + { + tooltip: 'Android Kotlin', + icon: '/docs/img/icons/kotlin-icon', + href: '/guides/getting-started/quickstarts/kotlin', + }, { tooltip: 'SvelteKit', icon: '/docs/img/icons/svelte-icon', @@ -69,7 +74,7 @@ const HomePageCover = (props) => {
    - +

    Getting Started

    diff --git a/apps/docs/components/LinkCard.tsx b/apps/docs/components/LinkCard.tsx index e74ac68384a..6e22200bbda 100644 --- a/apps/docs/components/LinkCard.tsx +++ b/apps/docs/components/LinkCard.tsx @@ -14,7 +14,7 @@ export default function LinkCard({ }) { return ( -
    + {description ? ( <>

    {title}

    diff --git a/apps/docs/components/Navigation/Footer.tsx b/apps/docs/components/Navigation/Footer.tsx index 32f1b4f73db..6550b0f24ce 100644 --- a/apps/docs/components/Navigation/Footer.tsx +++ b/apps/docs/components/Navigation/Footer.tsx @@ -29,11 +29,7 @@ const Footer = () => ( {Icon && }

    {text}

    -
    + {ctaLabel} diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 66179adaece..f12d0ecd7f8 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -72,12 +72,6 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [ href: '/guides/platform', level: 'platform', }, - { - label: 'Resources', - icon: 'resources', - href: '/guides/resources', - level: 'resources', - }, { label: 'Self-Hosting', icon: 'self-hosting', @@ -130,7 +124,25 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [ community: true, }, { - label: 'Tools', + label: 'Resources', + }, + { + label: 'CLI Commands', + icon: 'reference-cli', + href: '/reference/cli/introduction', + level: 'reference_javascript', + }, + { + label: 'Management API', + icon: 'reference-api', + href: '/reference/api/introduction', + level: 'reference_javascript', + }, + { + label: 'Guides and Examples', + icon: 'resources', + href: '/guides/resources', + level: 'resources', }, { label: 'Integrations', @@ -139,12 +151,6 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [ href: 'https://supabase.com/partners/integrations', level: 'integrations', }, - { - label: 'Management API', - icon: 'reference-api', - href: '/reference/api/introduction', - level: 'reference_javascript', - }, ], [ { @@ -550,6 +556,10 @@ export const database: NavMenuConstant = { name: 'Postgres Guides', url: undefined, items: [ + { + name: 'Analyzing efficiency and performance', + url: '/guides/database/inspect', + }, { name: 'JSON and unstructured data', url: '/guides/database/json', @@ -663,10 +673,23 @@ export const database: NavMenuConstant = { name: 'RUM: inverted index for full-text search', url: '/guides/database/extensions/rum', }, + ], + }, + { + name: 'Foreign Data Wrappers', + url: undefined, + items: [ + { name: 'Overview', url: '/guides/database/extensions/wrappers/overview' }, + { name: 'Connecting to Airtable', url: '/guides/database/extensions/wrappers/airtable' }, + { name: 'Connecting to AWS S3', url: '/guides/database/extensions/wrappers/s3' }, + { name: 'Connecting to BigQuery', url: '/guides/database/extensions/wrappers/bigquery' }, { - name: 'wrappers: 3rd Party Integrations', - url: '/guides/database/extensions/wrappers', + name: 'Connecting to ClickHouse', + url: '/guides/database/extensions/wrappers/clickhouse', }, + { name: 'Connecting to Firebase', url: '/guides/database/extensions/wrappers/firebase' }, + { name: 'Connecting to Logflare', url: '/guides/database/extensions/wrappers/logflare' }, + { name: 'Connecting to Stripe', url: '/guides/database/extensions/wrappers/stripe' }, ], }, { @@ -897,6 +920,10 @@ export const ai: NavMenuConstant = { items: [ { name: 'Developing locally with Vecs', url: '/guides/ai/vecs-python-client' }, { name: 'Creating and managing collections', url: '/guides/ai/quickstarts/hello-world' }, + { + name: 'Generate Embeddings', + url: '/guides/ai/quickstarts/generate-text-embeddings', + }, { name: 'Text Deduplication', url: '/guides/ai/quickstarts/text-deduplication' }, { name: 'Face similarity search', url: '/guides/ai/quickstarts/face-similarity' }, ], @@ -983,16 +1010,34 @@ export const supabase_cli: NavMenuConstant = { title: 'Local Dev / CLI', url: '/guides/cli', items: [ - { name: 'Getting started', url: '/guides/cli' }, - { name: 'Local Development', url: '/guides/cli/local-development' }, - { name: 'Managing environments', url: '/guides/cli/managing-environments' }, + { name: 'Overview', url: '/guides/cli' }, { - name: 'Managing config and secrets', - url: '/guides/cli/managing-config', + name: 'Using the CLI', + url: undefined, + items: [ + { name: 'Getting started', url: '/guides/cli/getting-started' }, + { name: 'CLI Configuration', url: '/guides/cli/config' }, + ], }, { - name: 'Testing emails locally', - url: '/guides/cli/testing-emails', + name: 'Developing with Supabase', + url: undefined, + items: [ + { name: 'Local Development', url: '/guides/cli/local-development' }, + { name: 'Managing environments', url: '/guides/cli/managing-environments' }, + { + name: 'Managing config and secrets', + url: '/guides/cli/managing-config', + }, + { + name: 'Seeding your database', + url: '/guides/cli/seeding-your-database', + }, + { + name: 'Testing and linting', + url: '/guides/cli/testing-and-linting', + }, + ], }, { name: 'GitHub Action', @@ -1002,14 +1047,14 @@ export const supabase_cli: NavMenuConstant = { name: 'Generate types from your database', url: '/guides/cli/github-action/generating-types', }, - ], - }, - { - name: 'Reference', - url: undefined, - items: [ - { name: 'Commands', url: '/reference/cli/introduction' }, - { name: 'Configuration', url: '/reference/cli/config' }, + { + name: 'Automated testing', + url: '/guides/cli/github-action/testing', + }, + { + name: 'Backup your database', + url: '/guides/cli/github-action/backups', + }, ], }, ], diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx index deb8caf1f17..8c88d731560 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuCliList.tsx @@ -23,7 +23,7 @@ const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => { return (
  • - + {icon && } {title} @@ -100,7 +100,7 @@ const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => {
    diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index f264a4112b7..0d390a0e6af 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -19,7 +19,7 @@ const HeaderLink = React.memo(function HeaderLink(props: { className={[ ' ', !props.title && 'capitalize', - props.url === router.pathname ? 'text-brand-900' : 'hover:text-brand-900 text-scale-1200', + props.url === router.pathname ? 'text-brand' : 'hover:text-brand text-scale-1200', ].join(' ')} > {props.title ?? props.id} @@ -68,7 +68,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any 'flex items-center gap-2', 'cursor-pointer transition text-sm', activeItem - ? 'text-brand-900 font-medium' + ? 'text-brand font-medium' : 'hover:text-scale-1200 dark:hover:text-scale-1100 text-scale-1000', ].join(' ')} parent={props.subItem.parent} @@ -97,8 +97,8 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any className={[ 'cursor-pointer transition text-sm', subSubItem.url === router.pathname - ? 'text-brand-900' - : 'hover:text-brand-900 text-scale-1000', + ? 'text-brand' + : 'hover:text-brand text-scale-1000', ].join(' ')} > {subSubItem.name} @@ -124,7 +124,7 @@ const ContentLink = React.memo(function ContentLink(props: any) { className={[ 'cursor-pointer transition text-sm', props.url === router.pathname - ? 'text-brand-900' + ? 'text-brand' : 'hover:text-scale-1200 dark:hover:text-scale-1100 text-scale-1000', ].join(' ')} > @@ -152,7 +152,7 @@ const Content = (props) => {
    @@ -166,7 +166,7 @@ const Content = (props) => { -
    +
    diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx index a280bf07da6..8c2007c78b3 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx @@ -16,7 +16,7 @@ import { deepFilterSections } from './NavigationMenu.utils' const HeaderLink = React.memo(function HeaderLink(props: any) { return ( - + {props.title ?? props.id} ) @@ -64,7 +64,7 @@ const FunctionLink = React.memo(function FunctionLink({ className={cn( 'cursor-pointer transition text-sm hover:text-scale-1200 gap-3 relative', isParent ? 'flex justify-between' : 'leading-3', - active ? 'text-brand-900' : 'text-scale-1000' + active ? 'text-brand' : 'text-scale-1000' )} > {icon && {icon}} @@ -72,7 +72,7 @@ const FunctionLink = React.memo(function FunctionLink({ {active && !isSubItem && ( )} {isParent && ( @@ -180,7 +180,7 @@ const NavigationMenuRefListItems = ({
    diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx index bd080d8e7c9..bba13421321 100644 --- a/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx @@ -56,7 +56,7 @@ const TopNavBarRef: FC = () => { } return ( -
    +{/* prettier-ignore */}
    - This reference documents every object and method available in Supabase's C# - library, [supabase-csharp](https://www.nuget.org/packages/supabase-csharp). You can - use supabase-csharp to interact with your Postgres database, listen to database changes, invoke - Deno Edge Functions, build login and user management functionality, and manage large files. + This reference documents every object and method available in Supabase's C# library, [supabase-csharp](https://www.nuget.org/packages/supabase-csharp). You can use supabase-csharp to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
    diff --git a/apps/docs/docs/reference/dart.mdx b/apps/docs/docs/reference/dart.mdx index 88095e50392..c947324bc8e 100644 --- a/apps/docs/docs/reference/dart.mdx +++ b/apps/docs/docs/reference/dart.mdx @@ -17,8 +17,8 @@ You can use the `supabase-flutter` library to: ## For non-Flutter projects -We also have [supabase-dart](https://github.com/supabase-community/supabase-dart) for non-Flutter Dart projects, such as server-side Dart or Angular-Dart. -supabase-dart shares most of the APIs with supabase-flutter without being dependent on Flutter so that you can use Supabase anywhere you can run Dart! +{/* prettier-ignore */} +We also have [supabase-dart](https://github.com/supabase-community/supabase-dart) for non-Flutter Dart projects, such as server-side Dart or Angular-Dart. supabase-dart shares most of the APIs with supabase-flutter without being dependent on Flutter so that you can use Supabase anywhere you can run Dart! ## Additional Links diff --git a/apps/docs/layouts/SiteLayout.tsx b/apps/docs/layouts/SiteLayout.tsx index 59bead2a64d..94cd0ae0631 100644 --- a/apps/docs/layouts/SiteLayout.tsx +++ b/apps/docs/layouts/SiteLayout.tsx @@ -228,7 +228,7 @@ const HeaderLogo = memo(function HeaderLogo() { height={24} alt="Supabase Logo" /> - DOCS + DOCS ) diff --git a/apps/docs/layouts/guides/index.tsx b/apps/docs/layouts/guides/index.tsx index 1363448853c..130e76e2510 100644 --- a/apps/docs/layouts/guides/index.tsx +++ b/apps/docs/layouts/guides/index.tsx @@ -115,7 +115,7 @@ const Layout: FC = (props) => { ].join(' ')} > {props.meta.breadcrumb && ( -

    {props.meta.breadcrumb}

    +

    {props.meta.breadcrumb}

    )}
    = ({ icon, ...props }) => { return (
    {icon && ( -
    +
    {icon}
    )} diff --git a/apps/docs/layouts/tutorials/TutorialLayout.tsx b/apps/docs/layouts/tutorials/TutorialLayout.tsx index 47557e96b1b..65ded8cf428 100644 --- a/apps/docs/layouts/tutorials/TutorialLayout.tsx +++ b/apps/docs/layouts/tutorials/TutorialLayout.tsx @@ -61,10 +61,10 @@ const Layout: FC = (props: Props) => { ].join(' ')} >
    -

    Tutorials

    +

    Tutorials

    {props.meta.title}

    -
    +
    diff --git a/apps/docs/lib/mdx/plugins/remarkTabs.ts b/apps/docs/lib/mdx/plugins/remarkTabs.ts new file mode 100644 index 00000000000..3562c2a6eca --- /dev/null +++ b/apps/docs/lib/mdx/plugins/remarkTabs.ts @@ -0,0 +1,138 @@ +import { Content, Paragraph, Parent } from 'mdast' +import { MdxJsxFlowElement } from 'mdast-util-mdx' +import { Node } from 'unist' +import { visit } from 'unist-util-visit' + +/** + * Transforms PyMdown Tabs to Supabase Tabs. + * + * https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/ + */ +const remarkPyMdownTabs = function () { + return function transformer(root: Parent) { + visit(root, 'paragraph', (node: Paragraph, nodeIndex: number, parent: Parent) => { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i] + if (child.type !== 'text') { + continue + } + + // Look for 3 '=', followed by an optionally quoted title, + // followed by optional newlines of text + const match = child.value.match(/^\n*=== ("?)(.+)\1\n?((?:.|\n)*)/) + + if (!match) { + continue + } + + // Extract the tab title along with the remaining text + const [, , title, value] = match + + // Rewrite the node's value to remove the tab syntax + child.value = value + + // Extract sibling nodes that should be linked to this tab + const siblingsToNest = extractLinkedSiblings(parent, node, nodeIndex) + + const children: any[] = [...node.children.slice(i), ...siblingsToNest] + + const tabPanelElement: MdxJsxFlowElement = { + type: 'mdxJsxFlowElement', + name: 'TabPanel', + attributes: [ + { + type: 'mdxJsxAttribute', + name: 'id', + value: title, + }, + { + type: 'mdxJsxAttribute', + name: 'label', + value: title, + }, + ], + children, + } + + let nodesAdded = 0 + const previousNode = parent.children[nodeIndex - 1] + + if (previousNode?.type === 'mdxJsxFlowElement' && previousNode.name === 'Tabs') { + // Add TabPanel to existing Tabs component + previousNode.children.push(tabPanelElement) + + // Remove this node + parent.children.splice(nodeIndex, 1) + nodesAdded-- + } else { + // Create new Tabs components and add TabPanel + const tabsElement: MdxJsxFlowElement = { + type: 'mdxJsxFlowElement', + name: 'Tabs', + attributes: [ + { + type: 'mdxJsxAttribute', + name: 'scrollable', + }, + { + type: 'mdxJsxAttribute', + name: 'size', + value: 'small', + }, + { + type: 'mdxJsxAttribute', + name: 'type', + value: 'underlined', + }, + ], + children: [tabPanelElement], + } + + // Overwrite this paragraph node with Tabs component + parent.children.splice(nodeIndex, 1, tabsElement) + } + + // If this wasn't the first child of the paragraph, create + // a new paragraph before this with the previous text children + if (i > 0) { + const previousChildren = node.children.slice(0, i) + const paragraph: Paragraph = { + type: 'paragraph', + children: previousChildren, + } + parent.children.splice(nodeIndex, 0, paragraph) + + nodesAdded++ + } + + // Return the correct index for the next visit, since + // we may have added or removed an element in the array + return nodeIndex + nodesAdded + } + }) + } +} + +/** + * Identifies sibling nodes that should be linked to this admonition + * based on their indent level (ie. 4 spaces). + * + * Iterates through proceeding siblings until one is found that is + * not indented relative to the original node. + * + * Splices the discovered siblings out of the original parent and returns them. + */ +function extractLinkedSiblings(parent: Parent, node: Node, index: number, indentAmount = 4) { + const { column } = node.position.start + + let nextSibling: Content + let i = index + + do { + nextSibling = parent.children[++i] + } while (nextSibling?.position && nextSibling.position.start.column === column + indentAmount) + + return parent.children.splice(index + 1, i - index - 1) +} + +export default remarkPyMdownTabs diff --git a/apps/docs/package.json b/apps/docs/package.json index 5d79d59dddd..e3710ea9ba6 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -97,6 +97,7 @@ "remark-admonitions": "^1.2.1", "remark-gfm": "^3.0.1", "remark-slug": "^7.0.1", + "remark-emoji": "^3.1.2", "shiki": "^0.11.1", "sse.js": "^0.6.1", "ui": "*", diff --git a/apps/docs/pages/guides/ai.mdx b/apps/docs/pages/guides/ai.mdx index 2153bdde029..3efb8e9b537 100644 --- a/apps/docs/pages/guides/ai.mdx +++ b/apps/docs/pages/guides/ai.mdx @@ -14,6 +14,7 @@ The toolkit includes: - A [vector store](/docs/guides/ai/vector-columns) and embeddings support using Postgres and pgvector. - A [Python client](/docs/guides/ai/vecs-python-client) for managing unstructured embeddings. +- An [embedding generation](/docs/guides/ai/quickstarts/generate-text-embeddings) process using open source models directly in Edge Functions. - [Database migrations](/docs/guides/ai/examples/headless-vector-search#prepare-your-database) for managing structured embeddings. - Integrations with all popular AI providers, such as [OpenAI](/docs/guides/ai/examples/openai), [Hugging Face](/docs/guides/ai/hugging-face), [LangChain](/docs/guides/ai/langchain), and more. diff --git a/apps/docs/pages/guides/ai/concepts.mdx b/apps/docs/pages/guides/ai/concepts.mdx index c35a6e72aed..27d71515f33 100644 --- a/apps/docs/pages/guides/ai/concepts.mdx +++ b/apps/docs/pages/guides/ai/concepts.mdx @@ -7,7 +7,7 @@ export const meta = { sidebar_label: 'Concepts', } -Embeddings are a core concept when building AI and vector applications. +Embeddings are core to many AI and vector applications. This guide covers these concepts. If you prefer to get started right away, see our guide on [Generating Embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings). ## What are embeddings? @@ -48,7 +48,7 @@ In this example we only have 2 dimensions: the X and Y axis. In reality, we woul ## Using embeddings -Compared to our 2-dimensional example above, most embedding models will output many more dimensions. For example OpenAI's `text-embedding-ada-002` model outputs 1536 dimensions. +Compared to our 2-dimensional example above, most embedding models will output many more dimensions. For example the open source [`gte-small`](https://huggingface.co/Supabase/gte-small) model outputs 384 dimensions. Why is this useful? Once we have generated embeddings on multiple texts, it is trivial to calculate how similar they are using vector math operations like cosine distance. A common use case for this is search. Your process might look something like this: diff --git a/apps/docs/pages/guides/ai/engineering-for-scale.mdx b/apps/docs/pages/guides/ai/engineering-for-scale.mdx index 0621207930f..871d0a5059c 100644 --- a/apps/docs/pages/guides/ai/engineering-for-scale.mdx +++ b/apps/docs/pages/guides/ai/engineering-for-scale.mdx @@ -120,7 +120,7 @@ We can now create a foreign table to access the data in our secondary project. ```sql create foreign table docs ( id text not null, - embedding vector(1536), + embedding vector(384), metadata jsonb, url text ) diff --git a/apps/docs/pages/guides/ai/examples/nextjs-vector-search.mdx b/apps/docs/pages/guides/ai/examples/nextjs-vector-search.mdx index 017eb68ec95..a568300d26a 100644 --- a/apps/docs/pages/guides/ai/examples/nextjs-vector-search.mdx +++ b/apps/docs/pages/guides/ai/examples/nextjs-vector-search.mdx @@ -51,7 +51,7 @@ Let's prepare the database schema. We can use the "OpenAI Vector Search" quickst - Make sure you have the latest version of the [Supabase CLI installed](). + Make sure you have the latest version of the [Supabase CLI installed](/docs/guides/cli/getting-started). Initialize Supabase in the root directory of your app. diff --git a/apps/docs/pages/guides/ai/hugging-face.mdx b/apps/docs/pages/guides/ai/hugging-face.mdx index c198e7b2349..9bf295d42d5 100644 --- a/apps/docs/pages/guides/ai/hugging-face.mdx +++ b/apps/docs/pages/guides/ai/hugging-face.mdx @@ -2,14 +2,18 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { id: 'ai-hugging-face', - title: 'Hugging Face', + title: 'Hugging Face Inference API', description: 'Learn how to integrate hugging face models with Supabase', sidebar_label: 'Hugging Face', } [Hugging Face](https://huggingface.co) is an open source hub for AI/ML models and tools. With over 100,000 machine learning models available, Hugging Face provides a great way to integrate specialized AI & ML tasks into your application. -Hugging Face exposes an [Inference API](https://huggingface.co/inference-api) you can use to execute AI tasks remotely on Hugging Face servers. This opens the doors to using Hugging Face with languages like TypeScript and can be deployed using [Edge Functions](/docs/guides/functions). +There are 3 ways to use Hugging Face models in your application: + +1. Use the [Transformers](https://huggingface.co/docs/transformers/index) Python library to perform inference in a Python backend. +1. [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) directly in Edge Functions using Transformers.js. +1. Use Hugging Face's hosted [Inference API](https://huggingface.co/inference-api) to execute AI tasks remotely on Hugging Face servers. This guide will walk you through this approach. ## AI Tasks diff --git a/apps/docs/pages/guides/ai/quickstarts/generate-text-embeddings.mdx b/apps/docs/pages/guides/ai/quickstarts/generate-text-embeddings.mdx new file mode 100644 index 00000000000..ae9e4b2b3e9 --- /dev/null +++ b/apps/docs/pages/guides/ai/quickstarts/generate-text-embeddings.mdx @@ -0,0 +1,211 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'ai-generating-embeddings', + title: 'Generate Embeddings', + subtitle: 'Generate text embeddings using Edge Functions + Transformer.js.', + breadcrumb: 'AI Quickstarts', +} + +This guide will walk you through how to generate high quality text embeddings in [Edge Functions](/docs/guides/functions) using Transformers.js. Inference is performed directly in Edge Functions using open source Hugging Face models, so no external API is required. + +## What is Transformers.js? + +[Transformers.js](https://huggingface.co/docs/transformers.js/index) is a library that allows you to perform inference using the JavaScript runtime. You can run Hugging Face embedding models through Transformers.js directly in Supabase [Edge Functions](/docs/guides/functions). + +## Build the Edge Function + +Let's build an Edge Function that will accept an input string and generate an embedding for it. Edge Functions are server-side TypeScript HTTP endpoints that run on-demand closest to your users. + + + + + + + + Make sure you have the latest version of the [Supabase CLI installed](/docs/guides/cli/getting-started). + + Initialize Supabase in the root directory of your app and start your local stack. + + + + + + ```shell + supabase init + supabase start + ``` + + + + + + + + + + Create an Edge Function that we will use to generate embeddings. We'll call this `embed` (you can name this anything you like). + + This will create a new TypeScript file called `index.ts` under `./supabase/functions/embed`. + + + + + + ```shell + supabase functions new embed + ``` + + + + + + + + + + Let's modify the Edge Function to import the Transformers.js client and configure it for the Deno runtime. + + ```ts ./supabase/functions/embed/index.ts + import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' + import { env, pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.5.0' + + // Configuration for Deno runtime + env.useBrowserCache = false; + env.allowLocalModels = false; + ``` + + + + + + + + + + Next let's construct the `pipe()` function we'll use to generate the embeddings. We use the Transformer.js `pipeline()` function to create this. + + + + + + ```ts ./supabase/functions/embed/index.ts + const pipe = await pipeline( + 'feature-extraction', + 'Supabase/gte-small', + ); + ``` + + + + + + Note the two arguments we pass to `pipeline()`: + + 1. The first argument specifies the type of [inference task](https://huggingface.co/docs/transformers.js/index#tasks) to perform. `feature-extraction` is used for embedding generation. + 2. The second argument specifies which Hugging Face model to use for the embedding generation. Supabase officially supports models available on the [Supabase org](https://huggingface.co/Supabase). + + + + We intentially construct the pipeline before the `serve()` function. This allows us to reuse the pre-loaded model in future warm-start requests, significantly speeding up future queries. Note that you'll only notice this speedup after deploying your function to Supabase - requests in local development will always be a cold-start. + + + + + + + + + + + + Modify our `serve()` request handler to accept an `input` string from the POST request JSON body. + + Finally let's generate the embedding by: + + 1. Calling `pipe()` to perform the embedding generation + 2. Extracting the embedding from the output as a number array + 3. Returning it as a JSON response + + + + + + ```ts ./supabase/functions/embed/index.ts + serve(async (req) => { + // Extract input string from JSON body + const { input } = await req.json(); + + // Generate the embedding from the user input + const output = await pipe(input, { + pooling: 'mean', + normalize: true, + }); + + // Extract the embedding output + const embedding = Array.from(output.data); + + // Return the embedding + return new Response( + JSON.stringify({ embedding }), + { headers: { 'Content-Type': 'application/json' } } + ); + }); + ``` + + + + + + Note the two options we pass to `pipe()`: + + - `pooling`: The first option sets `pooling` to `mean`. Pooling referes to how token-level embedding representations are compressed into a single sentence embedding that reflects the meaning of the entire sentence. Average pooling is the most common type of pooling for sentence embeddings and is the method supported by Transformers.js. + - `normalize`: The second option tells Transformers.js to normalize the embedding vector so that it can be used with distance measures like dot product. A normalized vector means its length (magnitude) is 1 - also referred to as a unit vector. A vector is normalized by dividing each element by the vector's length (magnitude), which maintains its direction but changes its length to 1. + + + + + + + + + + To test the Edge Function, first start a local functions server. + + + + + + ```shell + supabase functions serve + ``` + + + + + + Then in a new shell, create an HTTP request using cURL and pass in your input in the JSON body. + + ```shell + curl --request POST 'http://localhost:54321/functions/v1/embed' \ + --header 'Authorization: Bearer ANON_KEY' \ + --header 'Content-Type: application/json' \ + --data '{ "input": "hello world" }' + ``` + + Be sure to replace `ANON_KEY` with your project's anonymous key. You can get this key by running `supabase status`. + + + + + + + +## Next steps + +- Learn more about [embedding concepts](/docs/guides/ai/concepts) +- [Store your embeddings](/docs/guides/ai/vector-columns) in a database + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/ai/vector-columns.mdx b/apps/docs/pages/guides/ai/vector-columns.mdx index 7c768606e0b..b263cb9b619 100644 --- a/apps/docs/pages/guides/ai/vector-columns.mdx +++ b/apps/docs/pages/guides/ai/vector-columns.mdx @@ -56,27 +56,37 @@ create table documents ( id serial primary key, title text not null, body text not null, - embedding vector(1536) + embedding vector(384) ); ``` -In the above SQL snippet, we create a `documents` table with a column called `embedding` (note this is just a regular Postgres column - you can name it whatever you like). We give the `embedding` column a `vector` data type with 1536 dimensions. Change this to the number of dimensions used in your vector application. For example, if you are generating embeddings using OpenAI's `text-embeddings-ada-002` model, you would set this number as 1536 since that model produces 1536 dimensions. +In the above SQL snippet, we create a `documents` table with a column called `embedding` (note this is just a regular Postgres column - you can name it whatever you like). We give the `embedding` column a `vector` data type with 384 dimensions. Change this to the number of dimensions produced by your embedding model. For example, if you are [generating embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) using the open source [`gte-small`](https://huggingface.co/Supabase/gte-small) model, you would set this number to 384 since that model produces 384 dimensions. + + + +In general, embeddings with fewer dimensions perform best. See our [analysis on fewer dimensions in pgvector](https://supabase.com/blog/fewer-dimensions-are-better-pgvector). + + ### Storing a vector / embedding -In this example we'll generate a vector using the OpenAI API client, then store it in the database using the Supabase JavaScript client. +In this example we'll generate a vector using Transformers.js, then store it in the database using the Supabase JavaScript client. ```js +import { pipeline } from '@xenova/transformers' +const generateEmbedding = await pipeline('feature-extraction', 'Supabase/gte-small') + const title = 'First post!' const body = 'Hello world!' -// Generate a vector using OpenAI -const embeddingResponse = await openai.createEmbedding({ - model: 'text-embedding-ada-002', - input: body, +// Generate a vector using Transformers.js +const output = await generateEmbedding(body, { + pooling: 'mean', + normalize: true, }) -const [{ embedding }] = embeddingResponse.data.data +// Extract the embedding output +const embedding = Array.from(output.data) // Store the vector in Postgres const { data, error } = await supabase.from('documents').insert({ @@ -90,7 +100,7 @@ This example uses the JavaScript Supabase client, but you can modify it to work ### Querying a vector / embedding -Similarity search is the most common use case for vectors. `pgvector` support 3 new operators for performing similarity search: +Similarity search is the most common use case for vectors. `pgvector` support 3 new operators for computing distance: | Operator | Description | | -------- | ---------------------- | @@ -98,13 +108,13 @@ Similarity search is the most common use case for vectors. `pgvector` support 3 | `<#>` | negative inner product | | `<=>` | cosine distance | -Choosing the right operator depends on your needs. If you are searching over OpenAI embeddings, OpenAI recommends using cosine similarity. For more information on how embeddings work and how they relate to each other, see [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings). +Choosing the right operator depends on your needs. Dot product tends to be the fastest if your vectors are normalized. For more information on how embeddings work and how they relate to each other, see [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings). Supabase client libraries like `supabase-js` connect to your Postgres instance via [PostgREST](docs/guides/getting-started/architecture#postgrest-api). PostgREST does not currently support `pgvector` similarity operators, so we'll need to wrap our query in a Postgres function and call it via the `rpc()` method: ```sql create or replace function match_documents ( - query_embedding vector(1536), + query_embedding vector(384), match_threshold float, match_count int ) @@ -140,9 +150,15 @@ const { data: documents } = await supabaseClient.rpc('match_documents', { }) ``` -In this example `embedding` would be another embedding you wish to compare against your table of pre-generated embedding documents. For example if you were building a search engine, every time the user submits their query you would first generate an embedding on the search query itself (using `openai.createEmbedding()`), then pass it into the above `rpc()` function to match. +In this example `embedding` would be another embedding you wish to compare against your table of pre-generated embedding documents. For example if you were building a search engine, every time the user submits their query you would first generate an embedding on the search query itself, then pass it into the above `rpc()` function to match. -Vectors and embedding can be used for much more than search. Learn more about embeddings at [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings). + + +Be sure to use embeddings produced from the same embedding model when calculating distance. Comparing embeddings from two different models will produce no meaningful result. + + + +Vectors and embeddings can be used for much more than search. Learn more about embeddings at [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings). ### Indexes diff --git a/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx b/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx index cbb1ae76649..0aaafb1f237 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx @@ -162,6 +162,8 @@ import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' import { cookies } from 'next/headers' import { NextResponse } from 'next/server' +export const dynamic = 'force-dynamic' + export async function GET(request) { const requestUrl = new URL(request.url) const code = requestUrl.searchParams.get('code') @@ -190,6 +192,8 @@ import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import type { Database } from '@/lib/database.types' +export const dynamic = 'force-dynamic' + export async function GET(request: NextRequest) { const requestUrl = new URL(request.url) const code = requestUrl.searchParams.get('code') @@ -369,13 +373,9 @@ TypeScript types can be [generated with the Supabase CLI](/docs/reference/javasc ### Server-side -The combination of [Server Components](https://nextjs.org/docs/getting-started/react-essentials#server-components) and [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions) can be used to trigger the authentication process from form submissions. +The combination of [Server Components](https://nextjs.org/docs/getting-started/react-essentials#server-components) and [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) can be used to trigger the authentication process from form submissions. - - -Next.js [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions) are currently in Alpha and may change. For now we recommend [triggering the authentication flow client-side](/docs/guides/auth/auth-helpers/nextjs#client-side) for production applications. - - +#### Sign Up Route -```jsx app/login.js -import { createServerActionClient } from '@supabase/auth-helpers-nextjs' -import { revalidatePath } from 'next/cache' +```jsx app/auth/sign-up.js +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' -export default async function Login() { - const handleSignUp = async (formData) => { - 'use server' - const email = formData.get('email') - const password = formData.get('password') +export const dynamic = 'force-dynamic' - const supabase = createServerActionClient({ cookies }) - await supabase.auth.signUp({ - email, - password, - options: { - emailRedirectTo: 'http://localhost:3000/auth/callback', - }, - }) +export async function POST(request) { + const requestUrl = new URL(request.url) + const formData = await request.formData() + const email = formData.get('email') + const password = formData.get('password') + const supabase = createRouteHandlerClient({ cookies }) - revalidatePath('/') - } + await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${requestUrl.origin}/auth/callback`, + }, + }) - const handleSignIn = async (formData) => { - 'use server' - const email = formData.get('email') - const password = formData.get('password') + return NextResponse.redirect(requestUrl.origin, { + status: 301, + }) +} +``` - const supabase = createServerActionClient({ cookies }) - await supabase.auth.signInWithPassword({ - email, - password, - }) + - revalidatePath('/') - } + - const handleSignOut = async () => { - 'use server' - const supabase = createServerActionClient({ cookies }) - await supabase.auth.signOut() - revalidatePath('/') - } +```tsx app/auth/sign-up.ts +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' +import type { Database } from '@/lib/database.types' + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + const requestUrl = new URL(request.url) + const formData = await request.formData() + const email = String(formData.get('email')) + const password = String(formData.get('password')) + const supabase = createRouteHandlerClient({ cookies }) + + await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${requestUrl.origin}/auth/callback`, + }, + }) + + return NextResponse.redirect(requestUrl.origin, { + status: 301, + }) +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. + + + + + + + + +Returning a `301` status redirects from a POST to a GET route + + + +#### Login Route + + + + +```jsx app/auth/login.js +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' + +export const dynamic = 'force-dynamic' + +export async function POST(request) { + const requestUrl = new URL(request.url) + const formData = await request.formData() + const email = formData.get('email') + const password = formData.get('password') + const supabase = createRouteHandlerClient({ cookies }) + + await supabase.auth.signInWithPassword({ + email, + password, + }) + + return NextResponse.redirect(requestUrl.origin, { + status: 301, + }) +} +``` + + + + + +```tsx app/auth/login.ts +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' + +import type { Database } from '@/lib/database.types' + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + const requestUrl = new URL(request.url) + const formData = await request.formData() + const email = String(formData.get('email')) + const password = String(formData.get('password')) + const supabase = createRouteHandlerClient({ cookies }) + + await supabase.auth.signInWithPassword({ + email, + password, + }) + + return NextResponse.redirect(requestUrl.origin, { + status: 301, + }) +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. + + + + + + + + +Returning a `301` status redirects from a POST to a GET route + + + +#### Logout Route + + + + +```jsx app/auth/logout.js +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' + +export const dynamic = 'force-dynamic' + +export async function POST(request) { + const requestUrl = new URL(request.url) + const supabase = createRouteHandlerClient({ cookies }) + + await supabase.auth.signOut() + + return NextResponse.redirect(`${requestUrl.origin}/login`, { + status: 301, + }) +} +``` + + + + + +```tsx app/auth/logout.ts +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' + +import type { Database } from '@/lib/database.types' + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + const requestUrl = new URL(request.url) + const supabase = createRouteHandlerClient({ cookies }) + + await supabase.auth.signOut() + + return NextResponse.redirect(`${requestUrl.origin}/login`, { + status: 301, + }) +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. + + + + + + +#### Login Page + + + + +```jsx app/login.jsx +export default function Login() { return ( -
    + + + - - - + + +
    ) } @@ -445,70 +636,21 @@ export default async function Login() { -```tsx app/login.ts -import { createServerActionClient } from '@supabase/auth-helpers-nextjs' -import { revalidatePath } from 'next/cache' -import { cookies } from 'next/headers' - -import type { Database } from '@/lib/database.types' - -export default async function Login() { - const handleSignUp = async (formData: FormData) => { - 'use server' - const email = String(formData.get('email')) - const password = String(formData.get('password')) - - const supabase = createServerActionClient({ cookies }) - await supabase.auth.signUp({ - email, - password, - options: { - emailRedirectTo: 'http://localhost:3000/auth/callback', - }, - }) - - revalidatePath('/') - } - - const handleSignIn = async (formData: FormData) => { - 'use server' - const email = String(formData.get('email')) - const password = String(formData.get('password')) - - const supabase = createServerActionClient({ cookies }) - await supabase.auth.signInWithPassword({ - email, - password, - }) - - revalidatePath('/') - } - - const handleSignOut = async () => { - 'use server' - const supabase = createServerActionClient({ cookies }) - await supabase.auth.signOut() - revalidatePath('/') - } - +```tsx app/login.tsx +export default function Login() { return ( -
    + + + - - - + +
    ) } ``` - - -TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createServerActionClient` to add type support to the Supabase client. - - -
    @@ -551,7 +693,7 @@ This allows for the Supabase client to be easily instantiated in the correct con import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' import { useEffect, useState } from 'react' -export default function Home() { +export default function Page() { const [todos, setTodos] = useState() const supabase = createClientComponentClient() @@ -582,7 +724,7 @@ import type { Database } from '@/lib/database.types' type Todo = Database['public']['Tables']['todos']['Row'] -export default function Home() { +export default function Page() { const [todos, setTodos] = useState(null) const supabase = createClientComponentClient() @@ -653,7 +795,9 @@ In order to use Supabase in Server Components, you need to have implemented the import { cookies } from 'next/headers' import { createServerComponentClient } from '@supabase/auth-helpers-nextjs' -export default async function Home() { +export const dynamic = 'force-dynamic' + +export default async function Page() { const supabase = createServerComponentClient({ cookies }) const { data } = await supabase.from('todos').select() return
    {JSON.stringify(data, null, 2)}
    @@ -670,6 +814,8 @@ import { createServerComponentClient } from '@supabase/auth-helpers-nextjs' import type { Database } from '@/lib/database.types' +export const dynamic = 'force-dynamic' + export default async function ServerComponent() { const supabase = createServerComponentClient({ cookies }) const { data } = await supabase.from('todos').select() @@ -724,6 +870,8 @@ import { cookies } from 'next/headers' import { createServerActionClient } from '@supabase/auth-helpers-nextjs' import { revalidatePath } from 'next/cache' +export const dynamic = 'force-dynamic' + export default async function NewTodo() { const addTodo = async (formData) => { 'use server' @@ -753,6 +901,8 @@ import { revalidatePath } from 'next/cache' import type { Database } from '@/lib/database.types' +export const dynamic = 'force-dynamic' + export default async function NewTodo() { const addTodo = async (formData: FormData) => { 'use server' @@ -801,11 +951,13 @@ TypeScript types can be [generated with the Supabase CLI](/docs/reference/javasc > -```jsx app/api/todos/route.jsx +```jsx app/api/todos/route.js import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' import { NextResponse } from 'next/server' import { cookies } from 'next/headers' +export const dynamic = 'force-dynamic' + export async function POST(request) { const { title } = await request.json() const supabase = createRouteHandlerClient({ cookies }) @@ -818,13 +970,15 @@ export async function POST(request) { -```tsx app/api/todos/route.tsx +```tsx app/api/todos/route.ts import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' import { NextResponse } from 'next/server' import { cookies } from 'next/headers' import type { Database } from '@/lib/database.types' +export const dynamic = 'force-dynamic' + export async function POST(request: Request) { const { title } = await request.json() const supabase = createRouteHandlerClient({ cookies }) @@ -846,6 +1000,258 @@ TypeScript types can be [generated with the Supabase CLI](/docs/reference/javasc See [refreshing session example](/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware) above. +### Edge Runtime + +The Next.js Edge Runtime allows you to host Server Components and Route Handlers from Edge nodes, serving the routes as close as possible to your user's location. + +A route can be configured to use the Edge Runtime by exporting a `runtime` variable set to `edge`. Additionally, the `cookies()` function must be called from the Edge route, before creating a Supabase Client. + +#### Server Components + + + + +```jsx app/page.jsx +import { cookies } from 'next/headers' +import { createServerComponentClient } from '@supabase/auth-helpers-nextjs' + +export const runtime = 'edge' +export const dynamic = 'force-dynamic' + +export default async function Page() { + const cookieStore = cookies() + + const supabase = createServerComponentClient({ + cookies: () => cookieStore, + }) + + const { data } = await supabase.from('todos').select() + return
    {JSON.stringify(data, null, 2)}
    +} +``` + +
    + + + +```tsx app/page.tsx +import { cookies } from 'next/headers' +import { createServerComponentClient } from '@supabase/auth-helpers-nextjs' + +import type { Database } from '@/lib/database.types' + +export const runtime = 'edge' +export const dynamic = 'force-dynamic' + +export default async function Page() { + const cookieStore = cookies() + + const supabase = createServerComponentClient({ + cookies: () => cookieStore, + }) + + const { data } = await supabase.from('todos').select() + return
    {JSON.stringify(data, null, 2)}
    +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createServerComponentClient` to add type support to the Supabase client. + + + +
    +
    + +#### Route Handlers + + + + +```jsx app/api/todos/route.js +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { NextResponse } from 'next/server' +import { cookies } from 'next/headers' + +export const runtime = 'edge' +export const dynamic = 'force-dynamic' + +export async function POST(request) { + const { title } = await request.json() + const cookieStore = cookies() + + const supabase = createRouteHandlerClient({ + cookies: () => cookieStore, + }) + + const { data } = await supabase.from('todos').insert({ title }).select() + return NextResponse.json(data) +} +``` + + + + + +```tsx app/api/todos/route.ts +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { NextResponse } from 'next/server' +import { cookies } from 'next/headers' + +import type { Database } from '@/lib/database.types' + +export const runtime = 'edge' +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + const { title } = await request.json() + const cookieStore = cookies() + + const supabase = createRouteHandlerClient({ + cookies: () => cookieStore, + }) + + const { data } = await supabase.from('todos').insert({ title }).select() + return NextResponse.json(data) +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createRouteHandlerClient` to add type support to the Supabase client. + + + + + + +### Static Routes + +Server Components and Route Handlers are static by default - data is fetched once at build time and the value is cached. Since the request to Supabase now happens at build time, there is no user, session or cookie to pass along with the request to Supabase. Therefore, the `createClient` function from `supabase-js` can be used to fetch data for static routes. + +#### Server Components + + + + +```jsx app/page.jsx +import { createClient } from '@supabase/supabase-js' + +export default async function Page() { + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY + ) + + const { data } = await supabase.from('todos').select() + return
    {JSON.stringify(data, null, 2)}
    +} +``` + +
    + + + +```tsx app/page.tsx +import { createClient } from '@supabase/supabase-js' + +import type { Database } from '@/lib/database.types' + +export default async function Page() { + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ) + + const { data } = await supabase.from('todos').select() + return
    {JSON.stringify(data, null, 2)}
    +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createClient` to add type support to the Supabase client. + + + +
    +
    + +#### Route Handlers + + + + +```jsx app/api/todos/route.js +import { createClient } from '@supabase/supabase-js' +import { NextResponse } from 'next/server' + +export async function POST(request) { + const { title } = await request.json() + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY + ) + + const { data } = await supabase.from('todos').insert({ title }).select() + return NextResponse.json(data) +} +``` + + + + + +```tsx app/api/todos/route.ts +import { createClient } from '@supabase/supabase-js' +import { NextResponse } from 'next/server' + +import type { Database } from '@/lib/database.types' + +export async function POST(request: Request) { + const { title } = await request.json() + + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ) + + const { data } = await supabase.from('todos').insert({ title }).select() + return NextResponse.json(data) +} +``` + + + +TypeScript types can be [generated with the Supabase CLI](/docs/reference/javascript/typescript-support) and passed to `createClient` to add type support to the Supabase client. + + + + + + ## More examples - [Build a Twitter Clone with the Next.js App Router and Supabase - free egghead course](https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb) diff --git a/apps/docs/pages/guides/auth/phone-login/twilio.mdx b/apps/docs/pages/guides/auth/phone-login/twilio.mdx index 60277debe66..01b16e358f9 100644 --- a/apps/docs/pages/guides/auth/phone-login/twilio.mdx +++ b/apps/docs/pages/guides/auth/phone-login/twilio.mdx @@ -329,7 +329,7 @@ In some cases, you may wish to use WhatsApp as a delivery channel instead. Here - You wish for a secure channel - Your users mostly use WhatsApp as a messaging platform -To make use of WhatsApp OTP, please complete the following steps: +WhatsApp OTPs can be used with Twilio Verify without additional configuration. To make use of WhatsApp OTP with Twilio Programmable Messaging, please complete the following steps: - Go through the [Twilio self sign up guide for WhatsApp](https://www.twilio.com/docs/whatsapp/self-sign-up) - Submit a template via the [Twilio Guide For Submitting WhatsApp templates](https://www.twilio.com/docs/whatsapp/tutorial/send-whatsapp-notification-messages-templates#creating-message-templates-and-submitting-them-for-approval) diff --git a/apps/docs/pages/guides/auth/phone-login/vonage.mdx b/apps/docs/pages/guides/auth/phone-login/vonage.mdx index 0e6a1b86061..77949c2d0fc 100644 --- a/apps/docs/pages/guides/auth/phone-login/vonage.mdx +++ b/apps/docs/pages/guides/auth/phone-login/vonage.mdx @@ -53,7 +53,7 @@ Now go to the Auth > Settings page in the Supabase dashboard (https://supabase.c You should see an option to enable Phone Signup. -Toggle it on, and copy the api key, api secret and optionally phone number values over from the Vonage dashboard. Click save. +Toggle it on, and copy the api key, api secret and phone number values over from the Vonage dashboard. Click save. Now the backend should be setup, we can proceed to add our client-side code! diff --git a/apps/docs/pages/guides/auth/social-login/auth-twitter.mdx b/apps/docs/pages/guides/auth/social-login/auth-twitter.mdx index d681313a3e3..381e453bb3b 100644 --- a/apps/docs/pages/guides/auth/social-login/auth-twitter.mdx +++ b/apps/docs/pages/guides/auth/social-login/auth-twitter.mdx @@ -42,7 +42,7 @@ Setting up Twitter logins for your application consists of 3 parts: - Turn `Enable 3-legged OAuth` ON. - Turn `Request email address from users` ON. - Enter your `Callback URL`. -- Enter your `Website URL`. +- Enter your `Website URL` (tip: try `http://127.0.0.1:port` or `http://www.localhost:port` during development) - Enter your `Terms of service URL`. - Enter your `Privacy policy URL`. - Click `Save`. diff --git a/apps/docs/pages/guides/cli.mdx b/apps/docs/pages/guides/cli.mdx index 38506dd1df0..3f1e9e524cd 100644 --- a/apps/docs/pages/guides/cli.mdx +++ b/apps/docs/pages/guides/cli.mdx @@ -1,162 +1,47 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { - title: 'Supabase CLI', - description: - 'The Supabase CLI provides tools to develop your project locally and deploy to the Supabase Platform.', - subtitle: - 'The Supabase CLI provides tools to develop your project locally and deploy to the Supabase Platform.', + id: 'cli', + title: 'Local Dev with CLI', + description: 'Developing locally using the Supabase CLI.', + subtitle: 'Developing locally using the Supabase CLI.', + sidebar_label: 'Overview', } -You can use the Supabase CLI to run the entire Supabase stack locally on your machine, simply by running `supabase init` (to create a new local project) and then `supabase start`. +You can use the Supabase CLI to run the entire Supabase stack locally on your machine, simply by running `supabase init` and then `supabase start`. The Supabase CLI provides tools to develop your project locally, deploy to the Supabase Platform, handle database migrations, and generate types directly from your database schema. -## Installing the Supabase CLI +## Resources - - +
    + {integrations.map((x) => ( + + ))} +
    -Install the CLI as dev dependency via [npm](https://www.npmjs.com/package/supabase): +export const integrations = [ + { + name: 'Supabase CLI', + description: + 'The Supabase CLI provides tools to develop manage your Supabase projects from your local machine.', + href: 'https://github.com/supabase/cli', + }, + { + name: 'GitHub Action', + description: ' A GitHub action for interacting with your Supabase projects using the CLI.', + href: 'https://github.com/supabase/setup-cli', + }, +] -```sh -npm install supabase --save-dev -``` - -Run the CLI by prefixing each command with `npx` (only applicable when installing through `npm`): - -```sh -npx supabase -``` - -
    - - -Install the CLI with [Homebrew](https://brew.sh): - -```sh -brew install supabase/tap/supabase -``` - - - - -Install the CLI with [Scoop](https://scoop.sh): - -```powershell -scoop bucket add supabase https://github.com/supabase/scoop-bucket.git -scoop install supabase -``` - - - - -The CLI is available through [Homebrew](https://brew.sh) and Linux packages. - -#### Homebrew - -```sh -brew install supabase/tap/supabase -``` - -#### Linux packages - -Linux packages are provided in [Releases](https://github.com/supabase/cli/releases). -To install, download the `.apk`/`.deb`/`.rpm` file depending on your package manager -and run one of the following: - -- `sudo apk add --allow-untrusted <...>.apk` -- `sudo dpkg -i <...>.deb` -- `sudo rpm -i <...>.rpm` - - -
    - -## Updating the Supabase CLI - -When a new [version](https://github.com/supabase/cli/releases) is released, you can update the CLI using the same methods. - - - - -```sh -npm update supabase --save-dev -``` - - - - -```sh -brew upgrade supabase -``` - - - - -```powershell -scoop update supabase -``` - - - - -```sh -brew upgrade supabase -``` - - - - -If you have any Supabase containers running locally, remember to restart them after upgrading to use the new features. - -```bash -npx supabase stop --no-backup -npx supabase start -``` - -## Running Supabase locally - -Inside the folder where you want to create your project, run: - -```bash -supabase init -``` - -This will create a new `supabase` folder. It's safe to commit this folder to your version control system. - -Now, to start the Supabase stack, run: - -```bash -supabase start -``` - -This takes time on your first run because the CLI needs to download the local Docker images. The CLI includes the entire Supabase toolset, and a few additional images that are useful for local development (like a local SMTP server and a database diff tool). - -The local development environment includes Supabase Studio, a graphical interface for working with your database, running by default on [localhost:54323](http://localhost:54323). - -![Local Studio](/docs/img/guides/cli/local-studio.png) - -When you are finished working on your Supabase project, you can stop the stack with: - -```bash -supabase stop -``` - -## See also - -- [Supabase CLI Reference](/docs/reference/cli/introduction) -- [Supabase CLI Configuration](/docs/reference/cli/config) - -export const Page = ({ children }) => +export const Page = ({ children }) => export default Page diff --git a/apps/docs/pages/guides/cli/config.tsx b/apps/docs/pages/guides/cli/config.tsx new file mode 100644 index 00000000000..6789c50ce74 --- /dev/null +++ b/apps/docs/pages/guides/cli/config.tsx @@ -0,0 +1,113 @@ +import specFile from '~/../../spec/cli_v1_config.yaml' assert { type: 'yml' } +import { Parameter } from '~/lib/refGenerator/refTypes' +import ReactMarkdown from 'react-markdown' +import GuidesTableOfContents from '~/components/GuidesTableOfContents' +import { Heading } from '~/components/CustomHTMLElements' +import Head from 'next/head' +import { CodeBlock } from 'ui' + +// Parameters are grouped on the page by tag +const TAGS = ['General', 'Auth', 'API', 'Database', 'Dashboard', 'Local', 'Edge-Functions'] + +const tocList = [] +const content = TAGS.map((tag) => { + tocList.push({ text: tag, link: `${tag.toLowerCase()}-config`, level: 2 }) + return ( +
    + {tag} Config + {specFile.parameters + .filter((param: Parameter) => param.tags[0] === tag.toLowerCase()) + .map((parameter: Parameter, index) => { + tocList.push({ text: parameter.id, link: `#${parameter.id}`, level: 3 }) + return + })} +
    + ) +}) + +export default function Config() { + return ( + <> + + Supabase CLI config + +
    +
    +
    +

    CLI configuration

    +
    + {specFile.info.description} +
    {content}
    +
    +
    +
    +
    + + On this page + + +
    +
    +
    + + ) +} + +function Info({ parameter }: { parameter: Parameter }) { + return ( +
    +
    + + {parameter.title} + + +
    +
    +
    + + + + + + + + + + + + + + + +
    NameDefaultRequired
    {parameter.id}{parameter.default ? parameter.default.toString() : 'None'}{parameter.required.toString()}
    +
    +
    +

    Description

    + {parameter.description} +
    + {parameter.usage && ( +
    +

    Usage

    + + {parameter.usage} + +
    + )} + {parameter.links && ( +
    +

    See also

    +
      + {parameter.links.map((link) => ( +
    • + {link.name} +
    • + ))} +
    +
    + )} +
    +
    +
    +
    + ) +} diff --git a/apps/docs/pages/guides/cli/getting-started.mdx b/apps/docs/pages/guides/cli/getting-started.mdx new file mode 100644 index 00000000000..dd01d4ffcf7 --- /dev/null +++ b/apps/docs/pages/guides/cli/getting-started.mdx @@ -0,0 +1,172 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + title: 'Supabase CLI', + description: + 'The Supabase CLI provides tools to develop your project locally and deploy to the Supabase Platform.', + subtitle: + 'The Supabase CLI provides tools to develop your project locally and deploy to the Supabase Platform.', +} + +You can use the Supabase CLI to run the entire Supabase stack locally on your machine, simply by running `supabase init` (to create a new local project) and then `supabase start`. + +The Supabase CLI provides tools to develop your project locally, deploy to the Supabase Platform, handle database migrations, and generate types directly from your database schema. + +## Installing the Supabase CLI + + + + +Install the CLI as dev dependency via [npm](https://www.npmjs.com/package/supabase): + +```sh +npm install supabase --save-dev +``` + +Run the CLI by prefixing each command with `npx` (only applicable when installing through `npm`): + +```sh +npx supabase +``` + + + + +Install the CLI with [Homebrew](https://brew.sh): + +```sh +brew install supabase/tap/supabase +``` + + + + +Install the CLI with [Scoop](https://scoop.sh): + +```powershell +scoop bucket add supabase https://github.com/supabase/scoop-bucket.git +scoop install supabase +``` + + + + +The CLI is available through [Homebrew](https://brew.sh) and Linux packages. + +#### Homebrew + +```sh +brew install supabase/tap/supabase +``` + +#### Linux packages + +Linux packages are provided in [Releases](https://github.com/supabase/cli/releases). +To install, download the `.apk`/`.deb`/`.rpm` file depending on your package manager +and run one of the following: + +- `sudo apk add --allow-untrusted <...>.apk` +- `sudo dpkg -i <...>.deb` +- `sudo rpm -i <...>.rpm` + + + + +## Updating the Supabase CLI + +When a new [version](https://github.com/supabase/cli/releases) is released, you can update the CLI using the same methods. + + + + +```sh +npm update supabase --save-dev +``` + + + + +```sh +brew upgrade supabase +``` + + + + +```powershell +scoop update supabase +``` + + + + +```sh +brew upgrade supabase +``` + + + + +If you have any Supabase containers running locally, remember to restart them after upgrading to use the new features. + +```bash +npx supabase stop --no-backup +npx supabase start +``` + +## Running Supabase locally + +Inside the folder where you want to create your project, run: + +```bash +supabase init +``` + +This will create a new `supabase` folder. It's safe to commit this folder to your version control system. + +Now, to start the Supabase stack, run: + +```bash +supabase start +``` + +This takes time on your first run because the CLI needs to download the local Docker images. The CLI includes the entire Supabase toolset, and a few additional images that are useful for local development (like a local SMTP server and a database diff tool). + +The local development environment includes Supabase Studio, a graphical interface for working with your database, running by default on [localhost:54323](http://localhost:54323). + +![Local Studio](/docs/img/guides/cli/local-studio.png) + +## Stopping local services + +When you are finished working on your Supabase project, you can stop the stack with: + +```bash +supabase stop +``` + +## Full command reference + +The CLI provides a number of commands to help you develop your project locally and deploy to the Supabase Platform. You can find all commands inside the [CLI Reference](/docs/reference/cli/introduction) docs, including: + +- [Project](/docs/reference/cli/supabase-projects) and [Organization](/docs/reference/cli/supabase-orgs) management +- [Database management](/docs/reference/cli/supabase-db) +- [Database migrations](/docs/reference/cli/supabase-migration) and [Database Branching](/docs/reference/cli/supabase-branches) +- [Database debugigng tools](/docs/reference/cli/supabase-inspect-db-calls) +- [Edge Function management](/docs/reference/cli/supabase-functions) +- [Auth management](/docs/reference/cli/supabase-functions) +- [Types Generators](/docs/reference/cli/supabase-gen) +- [Testing](/docs/reference/cli/supabase-test) + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/cli/github-action/[slug].tsx b/apps/docs/pages/guides/cli/github-action/[slug].tsx index 181f3acb409..049e3f1072d 100644 --- a/apps/docs/pages/guides/cli/github-action/[slug].tsx +++ b/apps/docs/pages/guides/cli/github-action/[slug].tsx @@ -11,6 +11,7 @@ import Layout from '~/layouts/DefaultGuideLayout' import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' +import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' // We fetch these docs at build time from an external repo const org = 'supabase' @@ -24,13 +25,31 @@ const pageMap = [ { slug: 'generating-types', meta: { - title: 'Generate types from your database', + title: 'Generate types using GitHub Actions', description: 'End-to-end type safety across client, server, and database.', subtitle: 'End-to-end type safety across client, server, and database.', tocVideo: 'VSNgAIObBdw', }, remoteFile: 'generating-types.md', }, + { + slug: 'testing', + meta: { + title: 'Automated testing using GitHub Actions', + description: 'Run your tests when you or your team make changes.', + subtitle: 'Run your tests when you or your team make changes.', + }, + remoteFile: 'testing.md', + }, + { + slug: 'backups', + meta: { + title: 'Automated backups using GitHub Actions', + description: 'Backup your database on a regular basis.', + subtitle: 'Backup your database on a regular basis.', + }, + remoteFile: 'backups.md', + }, ] interface ActionDocsProps { @@ -116,6 +135,7 @@ export const getStaticProps: GetStaticProps = async ({ params } remarkPlugins: [ remarkGfm, remarkMkDocsAdmonition, + remarkPyMdownTabs, [removeTitle, meta.title], [remarkCodeHike, codeHikeOptions], ], diff --git a/apps/docs/pages/guides/cli/seeding-your-database.mdx b/apps/docs/pages/guides/cli/seeding-your-database.mdx new file mode 100644 index 00000000000..18bfa143c50 --- /dev/null +++ b/apps/docs/pages/guides/cli/seeding-your-database.mdx @@ -0,0 +1,60 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'seeding-your-database', + title: 'Seeding your database', + description: + 'Populate your database with initial data for reproducible environments across local and testing.', + subtitle: + 'Populate your database with initial data for reproducible environments across local and testing.', +} + +When you run `supabase init` we create an empty `seed.sql` file in the root of the `supabase` folder. You can use this to populate your database with seed data. + +## What is seed data? + +Seeding is the process of populating a database with initial data, typically used to provide sample or default records for testing and development purposes. You can use this to create "reproducible environments" for local development, staging, and production. + +## Using the seed file + +The `seed.sql` file is run every time you run `supabase start` or `supabase db reset`. Seeding is done _after_ all the database migrations have been run. As a general rule, you should not add schema statements to your seed file, only data. + +You can add any SQL statements to this file. For example: + +```sql supabase/seed.sql +insert into countries + (name, code) +values + ('United States', 'US'), + ('Canada', 'CA'), + ('Mexico', 'MX'); +``` + +{/* prettier-ignore */} +{/* + +### Splitting up your seed file + +The seed file can grow quite large, so you can split it up into multiple files. For example, you can create a `seed` folder and add a `countries.sql` file. Then you can add the following to your `seed.sql` file. + +```sql supabase/seed.sql +\i seed/countries.sql +``` + +### Using CSV data to seed your database + +co +You can use the `COPY` command to import CSV data into your database. For example, you can create a `seed` folder and add a `countries.csv` file. Then you can add the following to your `seed.sql` file. + +```sql supabase/seed.sql +COPY countries (name, code) FROM 'seed/countries.csv' WITH (FORMAT csv); +``` + +## Generating seed data from Production + +- Use snaplet + \*/} + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/cli/testing-and-linting.mdx b/apps/docs/pages/guides/cli/testing-and-linting.mdx new file mode 100644 index 00000000000..0099f84ca4e --- /dev/null +++ b/apps/docs/pages/guides/cli/testing-and-linting.mdx @@ -0,0 +1,82 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'testing-emails-locally', + title: 'Testing and linting', + description: 'Using the CLI to test your Supabase project.', + subtitle: 'Using the CLI to test your Supabase project.', +} + +The Supabase CLI provides a set of tools to help you test and lint your Postgres database and Edge` Functions. + +## Testing your database + +The Supabase CLI provides Postgres liniting using the `supabase test db` command. + +{/* prettier-ignore */} +```markdown +supabase test db --help +Tests local database with pgTAP + +Usage: + supabase test db [flags] +``` + +This is powered by the [pgTAP](https://supabase.com/docs/guides/database/extensions/pgtap) extension. You can find a full guide to writing an running tests in the [Testing your database](/docs/guides/database/testing) section. + +### Test helpers + +Our friends at [Basejump](https://usebasejump.com/) have created a useful set of Database [Test Helpers](https://github.com/usebasejump/supabase-test-helpers), with an accompanying [blog post](https://usebasejump.com/blog/testing-on-supabase-with-pgtap). + +### Running database tests in CI + +Use our GitHub Action to [automate your database tests](docs/guides/cli/github-action/testing#testing-your-database). + +## Testing your Edge Functions + +Edge Functions are powered by Deno, which provides a [native set of testing tools](https://deno.land/manual@v1.35.3/basics/testing). We extend this functionality in the Supabase CLI. You can find a detailed guide in the [Edge Functions section](/docs/guides/functions/unit-test). + +## Testing Auth emails + +The Supabase CLI uses [Inbucket](https://github.com/inbucket/inbucket) to capture emails sent from your local machine. This is useful for testing emails sent from Supabase Auth. + +### Accessing Inbucket + +By default, Inbucket is available at [localhost:54324](http://localhost:54324) when you run `supabase start`. Simply open this URL in your browser to view the emails. + +### Going into Production + +The "default" email provided by Supabase is only for development purposes. It is [heavily restricted](/docs/guides/platform/going-into-prod#auth-rate-limits) to ensure that it is not used for spam. Before going into production, you must configure your own email provider. This is as simple as enabling a new SMTP credentials in your [project settings](https://supabase.com/dashboard/project/_/settings/auth). + +## Linting your database + +The Supabase CLI provides Postgres liniting using the `supabase db lint` command: + +{/* prettier-ignore */} +```markdown +supabase db lint --help +Checks local database for typing error + +Usage: + supabase db lint [flags] + +Flags: + --level [ warning | error ] Error level to emit. (default warning) + --linked Lints the linked project for schema errors. + -s, --schema strings List of schema to include. (default all) +``` + +This is powered by [plpgsql_check](https://github.com/okbob/plpgsql_check), which leverages the internal Postgres parser/evaluator so you see any errors that would occur at runtime. It provides the following features: + +- validates you are using the correct types for function parameters +- identifies unused variables and function arguments +- detection of dead code (any code after an `RETURN` command) +- detection of missing `RETURN` commands with your Postgres function +- identifies unwanted hidden casts, which can be a performance issue +- checks `EXECUTE` statements agaist SQL injection vulnerability + +Check the Reference Docs for [more information](/docs/reference/cli/supabase-db-lint). + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/cli/testing-emails.mdx b/apps/docs/pages/guides/cli/testing-emails.mdx deleted file mode 100644 index f9e3cce6706..00000000000 --- a/apps/docs/pages/guides/cli/testing-emails.mdx +++ /dev/null @@ -1,24 +0,0 @@ -import Layout from '~/layouts/DefaultGuideLayout' - -export const meta = { - id: 'testing-emails-locally', - title: 'Testing emails locally', - description: 'Testing emails for Supabase Auth on your local machine.', - subtitle: 'Testing emails for Supabase Auth on your local machine.', -} - -The Supabase CLI uses [Inbucket](https://github.com/inbucket/inbucket) to capture emails sent from your local machine. This is useful for testing emails sent from Supabase Auth. - -## Accessing Inbucket - -By default, Inbucket is available at [localhost:54324](http://localhost:54324) when you run `supabase start`. Simply open this URL in your browser to view the emails. - -## Going into Production - -The "default" email provided by Supabase is only for development purposes. It is [heavily restricted](/docs/guides/platform/going-into-prod#auth-rate-limits) to ensure that it is not used for spam. - -Before you go into production, you must configure your own email provider. This is as simple as enabling a new SMTP credentials in your [project settings](https://supabase.com/dashboard/project/_/settings/auth). - -export const Page = ({ children }) => - -export default Page diff --git a/apps/docs/pages/guides/database/extensions/pgtap.mdx b/apps/docs/pages/guides/database/extensions/pgtap.mdx index 42d4521687e..991d15097b5 100644 --- a/apps/docs/pages/guides/database/extensions/pgtap.mdx +++ b/apps/docs/pages/guides/database/extensions/pgtap.mdx @@ -72,8 +72,8 @@ API: begin; select plan( 2 ); -select has_column( 'profiles', 'id' ); # test that the "id" column exists in the "profiles" table -select col_is_pk( 'profiles', 'id' ); # test that the "id" column is a primary key +select has_column( 'profiles', 'id' ); -- test that the "id" column exists in the "profiles" table +select col_is_pk( 'profiles', 'id' ); -- test that the "id" column is a primary key select * from finish(); rollback; @@ -94,8 +94,8 @@ select policies_are( 'public', 'profiles', ARRAY [ - 'Profiles are public', # Test that there is a policy called "Profiles are public" on the "profiles" table. - 'Profiles can only be updated by the owner' # Test that there is a policy called "Profiles can only be updated by the owner" on the "profiles" table. + 'Profiles are public', -- Test that there is a policy called "Profiles are public" on the "profiles" table. + 'Profiles can only be updated by the owner' -- Test that there is a policy called "Profiles can only be updated by the owner" on the "profiles" table. ] ); diff --git a/apps/docs/pages/guides/database/extensions/pgvector.mdx b/apps/docs/pages/guides/database/extensions/pgvector.mdx index b142920f358..534bbe63f99 100644 --- a/apps/docs/pages/guides/database/extensions/pgvector.mdx +++ b/apps/docs/pages/guides/database/extensions/pgvector.mdx @@ -19,7 +19,7 @@ Vector similarity refers to a measure of the similarity between two related item ### Embeddings -This is particularly useful if you're building on top of OpenAI's [GPT-3](https://openai.com/blog/gpt-3-apps/). You can create and store [embeddings](https://platform.openai.com/docs/guides/embeddings) which match the GPT model you're using. +This is particularly useful if you're building on top of OpenAI's [GPT-3](https://openai.com/blog/gpt-3-apps/). You can create and store [embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) for retrieval augmented generation. ## Usage @@ -66,25 +66,29 @@ create table posts ( id serial primary key, title text not null, body text not null, - embedding vector(1536) + embedding vector(384) ); ``` ### Storing a vector / embedding -In this example we'll generate a vector using the OpenAI API client, then store it in the database using the Supabase client. +In this example we'll generate a vector using Transformer.js, then store it in the database using the Supabase client. ```js +import { pipeline } from '@xenova/transformers' +const generateEmbedding = await pipeline('feature-extraction', 'Supabase/gte-small') + const title = 'First post!' const body = 'Hello world!' -// Generate a vector using OpenAI -const embeddingResponse = await openai.createEmbedding({ - model: 'text-embedding-ada-002', - input: body, +// Generate a vector using Transformers.js +const output = await generateEmbedding(body, { + pooling: 'mean', + normalize: true, }) -const [{ embedding }] = embeddingResponse.data.data +// Extract the embedding output +const embedding = Array.from(output.data) // Store the vector in Postgres const { data, error } = await supabase.from('posts').insert({ diff --git a/apps/docs/pages/guides/database/extensions/wrappers/[slug].tsx b/apps/docs/pages/guides/database/extensions/wrappers/[slug].tsx new file mode 100644 index 00000000000..2363a4ce7d0 --- /dev/null +++ b/apps/docs/pages/guides/database/extensions/wrappers/[slug].tsx @@ -0,0 +1,181 @@ +import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx' +import { GetStaticPaths, GetStaticProps } from 'next' +import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote' +import { serialize } from 'next-mdx-remote/serialize' +import { relative } from 'path' +import rehypeSlug from 'rehype-slug' +import remarkGfm from 'remark-gfm' +import emoji from 'remark-emoji' +import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' } +import components from '~/components' +import Layout from '~/layouts/DefaultGuideLayout' +import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' +import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' +import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' +import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' + +// We fetch these docs at build time from an external repo +const org = 'supabase' +const repo = 'wrappers' +const branch = 'main' +const docsDir = 'docs' +const externalSite = 'https://supabase.github.io/wrappers' + +// Each external docs page is mapped to a local page +const pageMap = [ + { + slug: 'airtable', + meta: { + title: 'Airtable', + }, + remoteFile: 'airtable.md', + }, + { + slug: 's3', + meta: { + title: 'AWS S3', + }, + remoteFile: 's3.md', + }, + { + slug: 'bigquery', + meta: { + title: 'BigQuery', + }, + remoteFile: 'bigquery.md', + }, + { + slug: 'clickhouse', + meta: { + title: 'ClickHouse', + }, + remoteFile: 'clickhouse.md', + }, + { + slug: 'firebase', + meta: { + title: 'Firebase', + }, + remoteFile: 'firebase.md', + }, + { + slug: 'logflare', + meta: { + title: 'Logflare', + }, + remoteFile: 'logflare.md', + }, + { + slug: 'stripe', + meta: { + title: 'Stripe', + }, + remoteFile: 'stripe.md', + }, +] + +interface WrappersDocsProps { + source: MDXRemoteSerializeResult + meta: { + title: string + description?: string + } +} + +export default function WrappersDocs({ source, meta }: WrappersDocsProps) { + return ( + + + + ) +} + +/** + * Fetch markdown from external repo and transform links + */ +export const getStaticProps: GetStaticProps = async ({ params }) => { + const page = pageMap.find(({ slug }) => slug === params.slug) + + if (!page) { + throw new Error(`No page mapping found for slug '${params.slug}'`) + } + + const { remoteFile, meta } = page + + const response = await fetch( + `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}` + ) + + const source = await response.text() + + const urlTransform: UrlTransformFunction = (url) => { + try { + const externalSiteUrl = new URL(externalSite) + + const placeholderHostname = 'placeholder' + const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`) + + // Don't modify a url with a FQDN or a url that's only a hash + if (hostname !== placeholderHostname || pathname === '/') { + return url + } + + const relativePage = ( + pathname.endsWith('.md') + ? pathname.replace(/\.md$/, '') + : relative(externalSiteUrl.pathname, pathname) + ).replace(/^\//, '') + + const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile) + + // If we have a mapping for this page, use the mapped path + if (page) { + return page.slug + hash + } + + // If we don't have this page in our docs, link to original docs + return `${externalSite}/${relativePage}${hash}` + } catch (err) { + console.error('Error transforming markdown URL', err) + return url + } + } + + const codeHikeOptions: CodeHikeConfig = { + theme: codeHikeTheme, + lineNumbers: true, + showCopyButton: true, + skipLanguages: [], + autoImport: false, + } + + const mdxSource = await serialize(source, { + scope: { + chCodeConfig: codeHikeOptions, + }, + mdxOptions: { + remarkPlugins: [ + remarkGfm, + remarkMkDocsAdmonition, + emoji, + remarkPyMdownTabs, + [removeTitle, meta.title], + [remarkCodeHike, codeHikeOptions], + ], + rehypePlugins: [[linkTransform, urlTransform], rehypeSlug], + }, + }) + + return { props: { source: mdxSource, meta } } +} + +export const getStaticPaths: GetStaticPaths = async () => { + return { + paths: pageMap.map(({ slug }) => ({ + params: { + slug, + }, + })), + fallback: false, + } +} diff --git a/apps/docs/pages/guides/database/extensions/wrappers.mdx b/apps/docs/pages/guides/database/extensions/wrappers/overview.mdx similarity index 78% rename from apps/docs/pages/guides/database/extensions/wrappers.mdx rename to apps/docs/pages/guides/database/extensions/wrappers/overview.mdx index 5d99f88ea08..16373b0a30e 100644 --- a/apps/docs/pages/guides/database/extensions/wrappers.mdx +++ b/apps/docs/pages/guides/database/extensions/wrappers/overview.mdx @@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { id: 'wrappers', - title: 'wrappers: 3rd Party Integrations', + title: 'Wrappers: 3rd Party Integrations', description: '3rd party integrations for PostgreSQL ', } @@ -60,12 +60,13 @@ A web interface for connecting to external data is coming to Supabase Studio in ## Integrations -- [Firebase](https://supabase.github.io/wrappers/firebase/) -- [Stripe](https://supabase.github.io/wrappers/stripe/) -- [BigQuery](https://supabase.github.io/wrappers/bigquery/) -- [AWS S3](https://supabase.github.io/wrappers/s3/) -- Airtable - Coming Soon -- Clickhouse - Coming Soon +- [Airtable](/docs/guides/database/extensions/wrappers/airtable) +- [AWS S3](/docs/guides/database/extensions/wrappers/s3) +- [BigQuery](/docs/guides/database/extensions/wrappers/bigquery) +- [ClickHouse](/docs/guides/database/extensions/wrappers/clickhouse) +- [Firebase](/docs/guides/database/extensions/wrappers/firebase) +- [Logflare](/docs/guides/database/extensions/wrappers/logflare) +- [Stripe](/docs/guides/database/extensions/wrappers/stripe) ## Resources diff --git a/apps/docs/pages/guides/database/inspect.mdx b/apps/docs/pages/guides/database/inspect.mdx new file mode 100644 index 00000000000..7cf71d50fd8 --- /dev/null +++ b/apps/docs/pages/guides/database/inspect.mdx @@ -0,0 +1,109 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'json', + title: 'Analyzing efficiency and performance from the terminal', + description: + 'Inspecting your Postgres database for common issues around disk, query performance, index, locks, and more using the terminal.', +} + +The Supabase CLI comes with a range of tools to help inspect your Postgres instances for potential issues. The CLI gets the information from Postgres internals. Therefore, most tools provided are compatible with any Postgres databases regardless if they are a Supabase project or not. + +You can find installation instructions for the the Supabase CLI here. + +### The `inspect db` command + +The inspection tools for your Postgres database are under then `inspect db` command. You can get a full list of available commands by running `supabase inspect db help`. + +``` +$ supabase inspect db help +Tools to inspect your Supabase database + +Usage: + supabase inspect db [command] + +Available Commands: + bloat Estimates space allocated to a relation that is full of dead tuples + blocking Show queries that are holding locks and the queries that are waiting for them to be released + cache-hit Show cache hit rates for tables and indices + +... +``` + +### Connect to any Postgres database + +Most inspection commands are Postgres agnostic. You can run inspection routines on any Postgres database even if it is not a Supabase project by providing a connection string via `--db-url`. + +For example you can connect to your local Postgres instance: + +``` +supabase --db-url postgresql://postgres:postgres@localhost:5432/postgres inspect db bloat +``` + +### Connect to a Supabase instance + +Working with Supabase, you can link the Supabase CLI with your project: + +``` +supabase link --project-ref +``` + +Then the CLI will automatically connect to your Supabase project whenever you are in the project folder and you no longer need to provide `—db-url`. + +## Inspection Commands + +Below are the db inspection commands provided, grouped by different use cases. + + + +Some commands might require `pg_stat_statements` to be enabled or a specific Postgres version to be used. + + + +### Disk Storage + +These commands are handy if you are running low on disk storage: + +- [bloat](/docs/reference/cli/supabase-inspect-db-bloat) - estimates the amount of wasted space +- [vacuum-stats](/docs/reference/cli/supabase-inspect-db-vacuum-stats) - gives information on waste collection routines +- [table-record-counts](/docs/reference/cli/supabase-inspect-db-table-record-counts) - estimates the number of records per table +- [table-sizes](/docs/reference/cli/supabase-inspect-db-table-sizes) - shows the sizes of tables +- [index-sizes](/docs/reference/cli/supabase-inspect-db-index-sizes) - shows the sizes of individual index +- [table-index-sizes](/docs/reference/cli/supabase-inspect-db-table-index-sizes) - shows the sizes of indexes for each table + +### Query Performance + +The commands below are useful if your Postgres database consumes a lot of resources like CPU, RAM or Disk IO. You can also use them to investigate slow queries. + +- [cache-hit](/docs/reference/cli/supabase-inspect-db-cache-hit) - shows how efficient your cache usage is overall +- [unused-indexes](/docs/reference/cli/supabase-inspect-db-unused-indexes) - shows indexes with low index scans +- [index-usage](/docs/reference/cli/supabase-inspect-db-index-usage) - shows information about the efficiency of indexes +- [seq-scans](/docs/reference/cli/supabase-inspect-db-seq-scans) - show number of sequential scans recorded against all tables +- [long-running-queries](/docs/reference/cli/supabase-inspect-db-long-running-queries) - shows long running queries that are executing right now +- [outliers](/docs/reference/cli/supabase-inspect-db-outliers) - shows queries with high execution time but low call count and queries with high proportion of execution time spent on synchronous I/O + +### Locks + +- [locks](/docs/reference/cli/supabase-inspect-db-locks) - shows statements which have taken out an exclusive lock on a relation +- [blocking](/docs/reference/cli/supabase-inspect-db-blocking) - shows statements that are waiting for locks to be released + +### Connections + +- [role-connections](/docs/reference/cli/supabase-inspect-db-role-connections) - shows number of active connections for all database roles (Supabase-specific command) +- [replication-slots](/docs/reference/cli/supabase-inspect-db-replication-slots) - shows information about replication slots on the database + +## Notes on `pg_stat_statements` + +Following commands require `pg_stat_statements` to be enabled: calls, locks, cache-hit, blocking, unused-indexes, index-usage, bloat, outliers, table-record-counts, replication-slots, seq-scans, vacuum-stats, long-running-queries. + +When using `pg_stat_statements` also take note that it only stores the latest 5,000 statements. Moreover, consider resetting the analysis after optimizing any queries by running `select pg_stat_statements_reset();` + +Learn more about pg_stats [here](https://supabase.com/docs/guides/database/extensions/pg_stat_statements). + +## Acknowledgements + +Supabase CLI's inspect commands are heavily inspired by the pg-extras tools. + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/getting-started/quickstarts/kotlin.mdx b/apps/docs/pages/guides/getting-started/quickstarts/kotlin.mdx index 91f5a535fa5..4b1991f3d12 100644 --- a/apps/docs/pages/guides/getting-started/quickstarts/kotlin.mdx +++ b/apps/docs/pages/guides/getting-started/quickstarts/kotlin.mdx @@ -62,8 +62,6 @@ export const meta = { ```kotlin implementation "io.github.jan-tennert.supabase:postgrest-kt:$supabase_version" implementation "io.ktor:ktor-client-android:$ktor_version" - implementation "io.ktor:ktor-client-core:$ktor_version" - implementation "io.ktor:ktor-utils:$ktor_version" ``` @@ -102,10 +100,8 @@ export const meta = { supabaseUrl = "https://xyzcompany.supabase.co", supabaseKey = "public-anon-key" ) { - install(GoTrue) - install(Postgrest) - install(Storage) - } + install(Postgrest) + } ``` diff --git a/apps/docs/pages/guides/platform/custom-postgres-config.mdx b/apps/docs/pages/guides/platform/custom-postgres-config.mdx index be25220006c..c5888bf6a6e 100644 --- a/apps/docs/pages/guides/platform/custom-postgres-config.mdx +++ b/apps/docs/pages/guides/platform/custom-postgres-config.mdx @@ -73,3 +73,15 @@ max_parallel_workers |3 | export const Page = ({ children }) => export default Page + +## Pooler Config + +You can also [customize some parameters](https://supabase.com/dashboard/project/_/settings/database) for the Connection Pooler: + +1. [Pooling Mode](https://devcenter.heroku.com/articles/best-practices-pgbouncer-configuration#pgbouncer-s-connection-pooling-modes) +1. [Default Pool Size](https://www.pgbouncer.org/config.html) +1. [Max Client Connections](https://www.pgbouncer.org/config.html) + +The default pool size, and the maximum number of clients allowed to connect concurrently is automatically optimized based on the compute add-on being used. At the moment, the Dashboard only reflects any custom configuration being used, and does not include the default optimized numbers used for your project. + +Custom Pooler Config may also require manual updates to any relevant overrides when changing compute add-ons. diff --git a/apps/docs/pages/index.mdx b/apps/docs/pages/index.mdx index 8024c9629a4..68b70f43e4b 100644 --- a/apps/docs/pages/index.mdx +++ b/apps/docs/pages/index.mdx @@ -25,7 +25,7 @@ export const meta = { ## Products
    -
    +
    {products.map((product) => { return ( @@ -44,7 +44,7 @@ export const meta = {
    -
    +
    {/* */} @@ -89,7 +89,7 @@ export const meta = {
    @@ -148,7 +148,7 @@ export const meta = {
    -
    +
    @@ -160,7 +160,7 @@ export const meta = {
    diff --git a/apps/docs/pages/reference/cli/config.tsx b/apps/docs/pages/reference/cli/config.tsx deleted file mode 100644 index 096aa30b6aa..00000000000 --- a/apps/docs/pages/reference/cli/config.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import specFile from '~/../../spec/cli_v1_config.yaml' assert { type: 'yml' } -import { Parameter } from '~/lib/refGenerator/refTypes' -import ReactMarkdown from 'react-markdown' -import GuidesTableOfContents from '~/components/GuidesTableOfContents' -import { Heading } from '~/components/CustomHTMLElements' -import Head from 'next/head' - -// Parameters are grouped on the page by tag -const TAGS = ['general', 'auth', 'api', 'database', 'dashboard', 'local', 'edge-functions'] - -const tocList = TAGS.map((tag) => - specFile.parameters - .filter((param: Parameter) => param.tags[0] === tag) - .map((parameter) => { - const text = parameter.id - const link = `#${parameter.id}` - const level = '2' - return { text, link, level } - }) -).flat() - -export default function Config() { - return ( - <> - - Supabase CLI config - -
    -
    -
    -

    CLI configuration

    -
    - {specFile.info.description} -
    - {TAGS.map((tag) => - specFile.parameters - .filter((param: Parameter) => param.tags[0] === tag) - .map((parameter: Parameter, index) => ( -
    - {index === 0 &&

    {tag}

    } -
    -
    - - {parameter.title} - -
    -
    -
    -

    - {parameter.description} -

    -
    -
    - Required: {parameter.required.toString()} -
    -
    - Default: - - {parameter.default ? parameter.default.toString() : 'None'} - -
    -
    -
    - {parameter.links && - parameter.links.map((link) => ( -
    -

    See also:

    -
  • - {link.name} -
  • -
    - ))} -
    -
    -
    -
    -
    - )) - )} -
    -
    -
    -
    -
    - - On this page - - -
    -
    -
    - - ) -} diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index d4668694ba3..28f9bd2e278 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -14,6 +14,7 @@ Bobbie Soedirgo Chandana Anumula Chase Granberry Chris Copplestone +Chris Gwilliams Dave Wilson Div Arora Divit D diff --git a/apps/docs/styles/main.scss b/apps/docs/styles/main.scss index ec6e38a814e..ece45d88688 100644 --- a/apps/docs/styles/main.scss +++ b/apps/docs/styles/main.scss @@ -169,7 +169,7 @@ pre[class*='language-'] { // ToC styles .toc__menu-item--active { - color: var(--colors-brand9) !important; + color: hsl(var(--brand-default)) !important; } .video-container { diff --git a/apps/www/_blog/2023-07-25-supabase-lw8-hackathon.mdx b/apps/www/_blog/2023-07-25-supabase-lw8-hackathon.mdx index 25b402b4d9b..e11d5569661 100644 --- a/apps/www/_blog/2023-07-25-supabase-lw8-hackathon.mdx +++ b/apps/www/_blog/2023-07-25-supabase-lw8-hackathon.mdx @@ -46,8 +46,6 @@ There will be a winner and a runner-up prize for each category. Every team membe ## Submission -Make sure you [register here](https://madewithsupabase.com/hackathons/launch-week-8). - You should submit your project using [this form](https://madewithsupabase.com/hackathons/launch-week-8) before 11:59 pm Sunday midnight PT 14th August 2023. Extra points if you include a simple video demoing the app in the description. ## Judges diff --git a/apps/www/_blog/2023-08-01-react-native-storage.mdx b/apps/www/_blog/2023-08-01-react-native-storage.mdx new file mode 100644 index 00000000000..ca9f81d61a7 --- /dev/null +++ b/apps/www/_blog/2023-08-01-react-native-storage.mdx @@ -0,0 +1,625 @@ +--- +title: 'React Native file upload with Supabase Storage' +description: Learn how to implement authentication and file upload in a React Native app. +author: simon_grimm +image: 2023-08-01-react-native-storage/react-native-storage.png +thumb: 2023-08-01-react-native-storage/react-native-storage.png +tags: + - react-native + - storage +date: '2023-08-01' +toc_depth: 2 +--- + +If you want to upload files from your React Native app, you need a backend to store the files, [Supabase Storage](https://supabase.com/docs/guides/storage) is a great choice for this as it provides a simple API to upload files, and we can easily combine this with authentication to build a powerful app. + +This means you can quickly build your own image-sharing app, a file-sharing app, or any other app that needs to upload files to a backend! + +![Supabase File Upload React Native](/images/blog/2023-08-01-react-native-storage/supabase-file-upload-react-native.gif) + +In this tutorial you will learn to: + +- Set up a React Native app with [Expo SDK49](https://expo.dev/) +- Use [Supabase Authentication](https://supabase.com/docs/guides/auth) +- Work with Expo Router v2 and protected routes +- Upload files to [Supabase Storage](https://supabase.com/docs/guides/storage) + +You can also directly check out the [full source code on Github](https://github.com/saimon24/react-native-resumable-upload-supabase) so you can get started with Supabase fast! + +Before we get into the app, let's quickly [start a new Supabase project](https://supabase.com/dashboard). + +## Creating the Supabase Project + +To use authentication and storage we need a new Supabase project. If you don't have a Supabase account yet, you can [get started for free](https://supabase.com/dashboard)! + +In your dashboard, click "New Project" and leave it to the default settings, but make sure you keep a copy of your Database password! + +![Supabase new project](/images/blog/2023-08-01-react-native-storage/new-project.png) + +After a minute your project should be ready, and we can configure the authentication and storage. + +## Setting up Authentication + +Authentication will be enabled by default, but we want to turn off email confirmation for this tutorial. + +Select **Authentication** from the menu, go to the **Providers** section, and expand Email. + +![Email Authentication settings](/images/blog/2023-08-01-react-native-storage/email-auth.png) + +Here you can disable the confirmation email, and apply other changes later if you want to! + +## Setting up Storage + +Now we want to create a **bucket** under storage where we will upload our files, and also add some security rules to protect the files of a user. + +First, select **Storage** from the menu, then click **New bucket** and call it `files`. + +![Supabase new bucket](/images/blog/2023-08-01-react-native-storage/new-bucket.png) + +Make sure that this is not a public bucket, otherwise, even unauthenticated users can upload or read the files! + +To protect that bucket and allow users only access to their own folder, we need to add some [Storage policies](https://supabase.com/docs/guides/storage/access-control). + +You can either do this through the UI and pick from examples, or simply run my SQL script in the **SQL Editor** which you can select from the menu: + +```sql +CREATE POLICY "Enable storage access for users based on user_id" ON "storage"."objects" +AS PERMISSIVE FOR ALL +TO public +USING (bucket_id = 'files' AND auth.uid()::text = (storage.foldername(name))[1]) +WITH CHECK (bucket_id = 'files' AND auth.uid()::text = (storage.foldername(name))[1]) +``` + +This will allow users to only access their own folder, and not any other files in the bucket. + +## Setting up the React Native app + +Now that we have our Supabase project ready, we can start building the React Native app! + +Get started by setting up a new Expo app with the tabs template and install some dependencies: + +```sh +# Create a new Expo app +npx create-expo-app@latest cloudApp --template tabs@49 + +# Install dependencies +npm i @supabase/supabase-js +npm i react-native-url-polyfill base64-arraybuffer react-native-loading-spinner-overlay + +# Install Expo packages +npx expo install expo-secure-store +npx expo install expo-image-picker +npx expo install expo-file-system +``` + +We will use the [Expo Secure Store](https://docs.expo.dev/versions/latest/sdk/securestore/) to store the Supabase session, and the [Expo Image Picker](https://docs.expo.dev/versions/latest/sdk/imagepicker/) to select images from the device. We also need the [Expo File System](https://docs.expo.dev/versions/latest/sdk/filesystem/) to read the image from the device and upload its data. + +You can now already run your project with `npx expo` and then select a platform to run on. + +However, the tabs template contains a lot of code that we don't need, so to simplify things we can remove the **app**, **constants** and **components** folder. + +This gives us a much cleaner project structure. + +## Connecting to Supabase from React Native + +To use Supabase we need to initialize the client with our project URL and the public key, which you can find in the **Settings** of your project under **API**. + +You can put both of them in a `.env` file at the root of your project: + +```env +EXPO_PUBLIC_SUPABASE_URL= +EXPO_PUBLIC_SUPABASE_ANON_KEY= +``` + +We can now simply read those values from the environment variables and initialize the Supabase client, so create a file at `config/initSupabase.ts` and add the following code: + +```ts +import * as SecureStore from 'expo-secure-store' +import 'react-native-url-polyfill/auto' + +import { createClient } from '@supabase/supabase-js' + +// Use a custom secure storage solution for the Supabase client to store the JWT +const ExpoSecureStoreAdapter = { + getItem: (key: string) => { + return SecureStore.getItemAsync(key) + }, + setItem: (key: string, value: string) => { + SecureStore.setItemAsync(key, value) + }, + removeItem: (key: string) => { + SecureStore.deleteItemAsync(key) + }, +} + +const url = process.env.EXPO_PUBLIC_SUPABASE_URL +const key = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY + +// Initialize the Supabase client +export const supabase = createClient(url, key, { + auth: { + storage: ExpoSecureStoreAdapter as any, + detectSessionInUrl: false, + }, +}) +``` + +We are using the SecureStore from Expo to handle the session of our Supabase client and add in the `createClient` function. + +Later we can import the `supabase` client from this file and use it in our app whenever we need to access Supabase. + +## Building the authentication flow + +Currently, the app won't work as we have no entry point. Because we are using the Expon Router and file-based routing, we can create a new file at `app/index.tsx` which will be the first page that comes up in our app. + +On this page we will handle both login and registration, so let's start by creating a simple form with a few inputs and buttons inside the `app/index.tsx` file: + +```tsx +import { Alert, View, Button, TextInput, StyleSheet, Text, TouchableOpacity } from 'react-native' +import { useState } from 'react' +import React from 'react' +import Spinner from 'react-native-loading-spinner-overlay' +import { supabase } from '../config/initSupabase' + +const Login = () => { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [loading, setLoading] = useState(false) + + // Sign in with email and password + const onSignInPress = async () => { + setLoading(true) + + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }) + + if (error) Alert.alert(error.message) + setLoading(false) + } + + // Create a new user + const onSignUpPress = async () => { + setLoading(true) + const { error } = await supabase.auth.signUp({ + email: email, + password: password, + }) + + if (error) Alert.alert(error.message) + setLoading(false) + } + + return ( + + + + My Cloud + + + + + + Sign in + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingTop: 200, + padding: 20, + backgroundColor: '#151515', + }, + header: { + fontSize: 30, + textAlign: 'center', + margin: 50, + color: '#fff', + }, + inputField: { + marginVertical: 4, + height: 50, + borderWidth: 1, + borderColor: '#2b825b', + borderRadius: 4, + padding: 10, + color: '#fff', + backgroundColor: '#363636', + }, + button: { + marginVertical: 15, + alignItems: 'center', + backgroundColor: '#2b825b', + padding: 12, + borderRadius: 4, + }, +}) + +export default Login +``` + +There's nothing fancy going on here, but this is all we need to use Supabase Authentication in our app! + +![Login page](/images/blog/2023-08-01-react-native-storage/login-page.png) + +You can try it out right now and create a new user account or sign in with an existing one and log the values to the console to see what's going on. + +However, we are not handling the authentication state yet so let's create a **Context** to listen to changes. + +We will wrap a Provider around our app, which will use the `onAuthStateChange` function from Supabase to listen to changes in the authentication state and accordingly set our state. + +For this, create a new file at `provider/AuthProvider.tsx` and add the following code: + +```tsx +import React, { useState, useEffect, createContext, PropsWithChildren } from 'react' +import { Session, User } from '@supabase/supabase-js' +import { supabase } from '../config/initSupabase' + +type AuthProps = { + user: User | null + session: Session | null + initialized?: boolean + signOut?: () => void +} + +export const AuthContext = createContext>({}) + +// Custom hook to read the context values +export function useAuth() { + return React.useContext(AuthContext) +} + +export const AuthProvider = ({ children }: PropsWithChildren) => { + const [user, setUser] = useState() + const [session, setSession] = useState(null) + const [initialized, setInitialized] = useState(false) + + useEffect(() => { + // Listen for changes to authentication state + const { data } = supabase.auth.onAuthStateChange(async (event, session) => { + setSession(session) + setUser(session ? session.user : null) + setInitialized(true) + }) + return () => { + data.subscription.unsubscribe() + } + }, []) + + // Log out the user + const signOut = async () => { + await supabase.auth.signOut() + } + + const value = { + user, + session, + initialized, + signOut, + } + + return {children} +} +``` + +To use the context we can now wrap it around our app, and while we do this we can also take care of the navigation: + +In the topmost layout file we can check whether a user has an active session or not, and either directly sign the user into the inside area (that we will create soon) or automatically bring her back to the login screen. + +To make this work with the Expo Router we can create a file at `app/_layout.tsx` and add the following code: + +```tsx +import { Slot, useRouter, useSegments } from 'expo-router' +import { useEffect } from 'react' +import { AuthProvider, useAuth } from '../provider/AuthProvider' + +// Makes sure the user is authenticated before accessing protected pages +const InitialLayout = () => { + const { session, initialized } = useAuth() + const segments = useSegments() + const router = useRouter() + + useEffect(() => { + if (!initialized) return + + // Check if the path/url is in the (auth) group + const inAuthGroup = segments[0] === '(auth)' + + if (session && !inAuthGroup) { + // Redirect authenticated users to the list page + router.replace('/list') + } else if (!session) { + // Redirect unauthenticated users to the login page + router.replace('/') + } + }, [session, initialized]) + + return +} + +// Wrap the app with the AuthProvider +const RootLayout = () => { + return ( + + + + ) +} + +export default RootLayout +``` + +Whenever the `initialized` or `session` state changes, we check if the user is authenticated and redirect her to the correct page. + +This also means we don't have to worry about the authentication state in our pages anymore, we can just assume that the user is authenticated and use the `useAuth` hook to access the user and session data later on. + +Your app might show an error right now because the `/list` route doesn't exist yet, but we will create it in the next step. + +## File Upload to Supabase Storage + +Now that we have the authentication set up, we can start working on the file upload. + +First, let's define another layout for this inside area so create a file at `/app/(auth)/_layout.tsx` and add the following code: + +```tsx +import { Stack } from 'expo-router' +import { useAuth } from '../../provider/AuthProvider' +import React from 'react' +import { TouchableOpacity } from 'react-native' +import { Ionicons } from '@expo/vector-icons' + +// Simple stack layout within the authenticated area +const StackLayout = () => { + const { signOut } = useAuth() + + return ( + + ( + + + + ), + }} + > + + ) +} + +export default StackLayout +``` + +This defines a simple stack navigation and adds a button to trigger the logout, so we can now also fully test the authentication flow. + +Next, we create the page for uploading and displaying all files of a user from Supabase Storage. + +You won't have any files to show yet, but loading the files of a user is as easy as calling `list()` on the storage bucket and passing the user id as the folder name. + +Additionally, we add a little FAB (floating action button) to trigger the file picker, so create a file at `/app/(auth)/list.tsx` and add the following code: + +```tsx +import { View, StyleSheet, TouchableOpacity, ScrollView } from 'react-native' +import React, { useEffect, useState } from 'react' +import { Ionicons } from '@expo/vector-icons' +import * as ImagePicker from 'expo-image-picker' +import { useAuth } from '../../provider/AuthProvider' +import * as FileSystem from 'expo-file-system' +import { decode } from 'base64-arraybuffer' +import { supabase } from '../../config/initSupabase' +import { FileObject } from '@supabase/storage-js' + +const list = () => { + const { user } = useAuth() + const [files, setFiles] = useState([]) + + useEffect(() => { + if (!user) return + + // Load user images + loadImages() + }, [user]) + + const loadImages = async () => { + const { data } = await supabase.storage.from('files').list(user!.id) + if (data) { + setFiles(data) + } + } + + const onSelectImage = async () => { + // TODO + } + + return ( + + {/* FAB to add images */} + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 20, + backgroundColor: '#151515', + }, + fab: { + borderWidth: 1, + alignItems: 'center', + justifyContent: 'center', + width: 70, + position: 'absolute', + bottom: 40, + right: 30, + height: 70, + backgroundColor: '#2b825b', + borderRadius: 100, + }, +}) + +export default list +``` + +This should give us a nice and clean UI. + +![Files page](/images/blog/2023-08-01-react-native-storage/files-page.png) + +Now we can implement the image picker and upload the selected image to Supabase Storage. + +Using the image picker from Expo gives us a **URI**, which we can use to read the file from the file system and convert it to a base64 string. + +We can then use the `upload()` method from the storage client to upload the file to the storage bucket. Life can be easy. + +At this point, you should be able to upload files to Supabase Storage, and you can already see them in your UI (or log them to the console). + +To finally display them we will add a `ScrollView` component, which will render one item for every file of a user. + +Let's start by creating those component rows in another file, so create a `components/ImageItem.tsx` file and add the following code: + +```tsx +import { FileObject } from '@supabase/storage-js' +import { Image, View, Text, TouchableOpacity } from 'react-native' +import { supabase } from '../config/initSupabase' +import { useState } from 'react' +import { Ionicons } from '@expo/vector-icons' + +// Image item component that displays the image from Supabase Storage and a delte button +const ImageItem = ({ + item, + userId, + onRemoveImage, +}: { + item: FileObject + userId: string + onRemoveImage: () => void +}) => { + const [image, setImage] = useState('') + + supabase.storage + .from('files') + .download(`${userId}/${item.name}`) + .then(({ data }) => { + const fr = new FileReader() + fr.readAsDataURL(data!) + fr.onload = () => { + setImage(fr.result as string) + } + }) + + return ( + + {image ? ( + + ) : ( + + )} + {item.name} + {/* Delete image button */} + + + + + ) +} + +export default ImageItem +``` + +This component will display the image from Supabase Storage, the name of the file and a delete button. + +To display the image we use the `download()` method from the storage client, which returns a `FileObject` with the file data. We can then use the `FileReader` to convert the file data to a base64 string, which we can use as the image source. + +Now let's use this component in our `list.tsx` file to render the list of images by updating the `return` statement: + +```tsx +return ( + + + {files.map((item, index) => ( + onRemoveImage(item, index)} + /> + ))} + + + {/* FAB to add images */} + + + + +) +``` + +Don't forget to also include the import to the `ImageItem` component! + +![Supabase Storage Files List](/images/blog/2023-08-01-react-native-storage/supabase-storage-files.png) + +Finally, we can also include the delete functionality by adding the following code to the `list.tsx`: + +```tsx +const onRemoveImage = async (item: FileObject, listIndex: number) => { + supabase.storage.from('files').remove([`${user!.id}/${item.name}`]) + const newFiles = [...files] + newFiles.splice(listIndex, 1) + setFiles(newFiles) +} +``` + +We are handling the deletion here so we can accordingly update the state of the files list after removing an item. + +And with that in place, you have a fully functional image gallery app with React Native and Supabase Storage! + +## What about resumable uploads? + +Initially, I wanted to include [resumable uploads](https://supabase.com/blog/storage-v3-resumable-uploads) in this tutorial, but apparently, the [Uppy](https://uppy.io/) client didn't work 100% for React Native yet. + +You can still see an [initial implementation of resumable downloads with Supabase and React Native](https://github.com/saimon24/react-native-resumable-upload-supabase/commit/bb45455dc2d4d3fdd42089a8a26a521cc5b5e60f#diff-2d97fcb7ad2b1ac426a8d5391cfd0ab970e525ede1f11e14885f65e6dcd116a1) in the repository of this tutorial. + +However, ultimately the uploaded file was always 0 bytes, so I decided to leave it out for now. + +The Supabase team is investigating this issue, so I'm very sure that we will have resumable uploads working with React Native soon. + +## Conclusion + +It's almost too easy to use Supabase Storage, and it's a great way to store files for your apps. + +You now have a fully functional image gallery app with React Native and Supabase Storage including user authentication without writing a line of backend code! + +You can [find the full code of this tutorial on Github](https://github.com/saimon24/react-native-resumable-upload-supabase) where you just need to insert your own Supabase URL and API key. + +If you enjoyed the tutorial, you can [learn React Native on Galaxies.dev](https://galaxies.dev) where I help developers build awesome React Native apps. + +Until next time and happy coding with Supabase! diff --git a/apps/www/_blog/2023-08-02-beta-update-july-2023.mdx b/apps/www/_blog/2023-08-02-beta-update-july-2023.mdx new file mode 100644 index 00000000000..2a6c49e301c --- /dev/null +++ b/apps/www/_blog/2023-08-02-beta-update-july-2023.mdx @@ -0,0 +1,105 @@ +--- +title: Supabase Beta July 2023 +description: Launch Week 8 is coming - but we still shipped some goodies during July +author: ant_wilson +image: 2023-08-02-july-2023-beta-update/july-2023-beta-update.png +thumb: 2023-08-02-july-2023-beta-update/july-2023-beta-update.png +tags: + - release-notes +date: '2023-08-02' +toc_depth: 3 +--- + +Next Monday is Launch Week 🎱, where we’ll be announcing a tonne of surprises. In the meantime, here’s a summary of everything we shipped in July. Read on! + +## Supabase Launch Week 8 + +![Supabase Launch Week 8](https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/marketing-emails/launch-week.png) + +Launch Week 8 starts next week. We’ll be announcing one new major feature every day for an entire week (with a few bonus surprises along the way). Get your ticket and tune in on Monday the 7th. + +[Get your ticket](https://supabase.com/launch-week) + +## New Android Kotlin Library + +![New Android Kotlin Library](https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/marketing-emails/android-kotlin.png?t=2023-08-02T11%3A05%3A46.805Z) + +Are you a mobile dev? We now have client libraries for Android. This is thanks to one of our community members - [@TheRealJanGER](https://twitter.com/TheRealJanGER). Try it out, and if you want to help us maintain the library let us know. + +[Quickstart](https://supabase.com/docs/guides/getting-started/quickstarts/kotlin) + +[Tutorial](https://supabase.com/docs/guides/getting-started/tutorials/with-kotlin) + +## pgvector 0.4.0 performance + +![pgvector 0.4.0 performance](https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/marketing-emails/pgvector.png) + +There's been a lot of talk about pgvector performance, so we pushed it to the limits to figure out its strengths and limitations. The good news is that work has already [started](https://github.com/pgvector/pgvector/tree/hnsw) on HNSW, and isn’t far away. + +[Read the benchmark](https://supabase.com/blog/pgvector-performance) + +## Quick product announcements + +- [Auth] There is a new OAuth provider in time: Login with Figma. [Docs](https://supabase.com/docs/guides/auth/social-login/auth-figma) +- [Auth] You can now use Resend as a custom SMTP provider. [Docs](https://resend.com/docs/send-with-supabase-smtp) +- [Postgres Tooling] pgsodium 3.1.8 released. [PR](https://github.com/michelp/pgsodium/releases/tag/v3.1.8) +- [Postgres Tooling] PostgREST 11.1 is now live. [Blog post](https://supabase.com/blog/postgrest-11-1-release) + +## Egghead course: Twitter Clone with the Next.js App Router + +![Egghead course: Twitter Clone with the Next.js App Router](https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/marketing-emails/egghead-course.png?t=2023-08-02T10%3A55%3A17.908Z) + +Should we rebrand to X Clone? Next.js App router is ushering in the next era of web development. What a best way to learn to navigate it than by building a full-stack app? + +[Course](https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb) + +## Twitter Space: Launch Week 8 - Kickoff 🏈 + +![Twitter Space](https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/marketing-emails/twitter-space.png?t=2023-08-02T11%3A05%3A03.838Z) + +After Monday's announcement, we are kicking off LW8 with a very cool Twitter Space, featuring the DevRel team and guests. Bring your questions! + +10:00 AM PT | 1:00 PM ET | 06:00 PM UK + +[Set reminder](https://twitter.com/i/spaces/1DXGyvjkeEeJM) + +## Made with Supabase + +![Made with Supabase: Snowchat](https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/marketing-emails/snowchat.png) + +[snowChat](https://snowchat.streamlit.app/) | An intuitive and user-friendly application that allows you to interact with your Snowflake data using natural language queries. Built by [Kaarthik Andavar](https://twitter.com/kaarthikcodes) using Python, Streamlit, Open AI, and Supabase Vector. + +## Extended Community Highlights + +![Community Highlights](https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/marketing-emails/community-highlights.png) + +- Build a Simple Dropbox clone using Deno, DenoKV, Fresh and Supabase. [Video Tutorial](https://www.youtube.com/watch?v=_WerF8YMqks) +- Safeguarding Data Integrity with pg_safeupdate in PostgreSQL and Supabase. [Blog post](https://www.notion.so/809925346af243b3a4ede8774e71472c?pvs=21) +- Supabase for C#: Streamlining Backend Development with Database Magic. [Blog post](https://themurph.hashnode.dev/supabase-csharp) +- Building an MVP app using Supabase. [Blog post](https://www.antstack.com/blog/building-an-mvp-app-using-supabase/) +- Fortify Your Application With Supabase: Exploring Fine-Grained Control with Row-Level Security and OAuth Sign-in. [Article](https://techconative.com/blog/supabase-rls-security-oauth-authentication/) +- Using Supabase authentication in NestJs. [Tutorial](https://blog.iamstarcode.com/using-supabase-authentication-in-nestjs) +- React Native file upload with Supabase Storage. [Tutorial](https://supabase.com/blog/react-native-storage) | [Video](https://www.youtube.com/watch?v=am6w5zEDk_g) +- Safeguarding Data Integrity with pg_safeupdate in PostgreSQL and Supabase. [Blog Post](https://blog.mansueli.com/safeguarding-data-integrity-with-pg-safeupdate-in-postgresql-and-supabase) +- Supabase almost SHUT ME DOWN. [Video](https://www.youtube.com/watch?v=2WHTZa4ENfc) + +## Supabase in Times Square + +![Supabase in Times Square](https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/marketing-emails/times-sq.png?t=2023-08-02T11%3A04%3A14.458Z) + +The Redpoint team put together InfraRed 100, a report that recognizes 100 transformative companies in cloud infrastructure. We are honoured to be included with so many amazing companies we admire 🙇🏻‍♂️. They even featured us on a billboard in Times Square to celebrate. + +[Read the full report](https://www.redpoint.com/infrared/100/) + +## We’re Hiring + +- [Support Engineer (APAC)](https://boards.greenhouse.io/supabase/jobs/4934378004) +- [FullStack Engineer Billing](https://www.notion.so/Beta-Update-July-Monday-31st-10386eba40934ea597ee5e5b1b81574e?pvs=21) + +## ⚠️ Baking hot meme zone ⚠️ + +If you made it this far in the email you deserve a devilish treat. + +NPCs represent… + +![July's favorite meme](https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/marketing-emails/supabase-meme.png) diff --git a/apps/www/_blog/2023-08-03-fewer-dimensions-are-better-pgvector.mdx b/apps/www/_blog/2023-08-03-fewer-dimensions-are-better-pgvector.mdx new file mode 100644 index 00000000000..8ff56d7e4f7 --- /dev/null +++ b/apps/www/_blog/2023-08-03-fewer-dimensions-are-better-pgvector.mdx @@ -0,0 +1,169 @@ +--- +title: 'pgvector: Fewer dimensions are better' +description: Increase performance in pgvector by using embedding vectors with fewer dimensions +author: gregnr,oli_rice,egor_romanov +tags: + - AI + - performance + - postgres + - planetpg +date: '2023-08-03' +toc_depth: 3 +image: 2023-08-03-fewer-dimensions-are-better-pgvector/fewer-dimensions-og.png +thumb: 2023-08-03-fewer-dimensions-are-better-pgvector/fewer-dimensions-thumb.png +--- + +Embeddings have become a core tool in AI applications to determine semantic similarity between pieces of content. Developers are using embeddings for various tasks, such as search, retrieval augmented generation, and clustering. + +At Supabase, we support storing embeddings in Postgres using the [pgvector](https://github.com/pgvector/pgvector) extension. pgvector adds a new datatype `vector`, which developers use to store embeddings in regular columns within their database tables. Similarity can be calculated between these vectors using one of the three supported distance measures: + +- Inner product +- Cosine distance +- Euclidean distance + +## Challenges with pgvector + +Without indexes, pgvector performs a full table scan when you run a similarity query. This means distance has to be computed against every row in your table. This is manageable at a small scale, but becomes problematic as your table grows. + +To solve this, pgvector offers indexes. Indexes reorganize the data into data structures that exploit internal structure and enable approximate similarity search without referring to every record. Currently, pgvector supports an IVF index, with HNSW expected in the next release. + +IVF [indexes](https://supabase.com/docs/guides/ai/managing-indexes) work by clustering vectors into `lists`, and then querying only vectors within the same list (or multiple nearby lists, depending on the value of `probes`). + +### Scaling indexes + +98% of our customers are generating text embeddings using OpenAI's `text-embedding-ada-002` model. At an initial glance, there's good reason for this - these embeddings perform quite well for information retrieval and are economical to produce. `text-embedding-ada-002` produces vectors with 1536 dimensions which is among the largest in the industry. IVF indexes help address some scaling challenges, but there are still some pitfalls. + +First, vectors are large. A 1536 dimensional vector is ~12.3 kilobytes. Scaling that up to 1M vectors, the raw data tops 11 gigabytes. Experienced SQL users know that for best performance, indexes should fit within system memory. Moreover unlike traditional workloads, there is a significant compute component when performing vector similarity queries. + +In the real-world it's common for an index to reduce the number of distance computations needed to estimate nearest neighbors from 100% of the dataset to 5-20%. At 1M records, that's still 50k-200k distance calculations being performed for a single query. Given how different the [resource requirements](https://supabase.com/docs/guides/ai/choosing-compute-addon) are to support heavy vector workloads, it's not surprising that one of the most common issues we see is significant under provisioning of hardware. + +## Massive Text Embedding Benchmark (MTEB) + +Text embeddings are one of the most common types of embeddings today. Our friends at Hugging Face have done the hard work of comparing text embedding models (including OpenAI's `text-embedding-ada-002`) by benchmarking them against common similarity tasks: + +- Retrieval (search) +- Semantic textual similarity (STS) +- Classification +- Clustering +- Pair classification +- Reranking +- Summarization +- Bitext mining + +Models are ranked per task by taking their average score produced over each task's datasets. Each model also has an overall (general purpose) score, calculated by taking the average score produced across all datasets. You can find the results on their [MTEB leaderboard](https://huggingface.co/spaces/mteb/leaderboard). + +It's worth pointing out that each model's dimension size has little-to-no correlation with its performance. In fact there are a number of models that perform comparably with`text-embedding-ada-002`, all of which produce embeddings with fewer dimensions than 1536. + +| Rank | Model | Dimensions | Average | Model Size (GB) | +| ---- | -------------------------------------------------------------------------------------------------- | ---------- | ------- | --------------- | +| 1 | [gte-large](https://huggingface.co/thenlper/gte-large) | 1024 | 63.13 | 0.67 | +| 2 | [gte-base](https://huggingface.co/thenlper/gte-base) | 768 | 62.39 | 0.22 | +| 3 | [e5-large-v2](https://huggingface.co/intfloat/e5-large-v2) | 1024 | 62.25 | 1.34 | +| 9 | [gte-small](https://huggingface.co/thenlper/gte-small) | 384 | 61.36 | 0.07 | +| 10 | [text-embedding-ada-002](https://beta.openai.com/docs/guides/embeddings/types-of-embedding-models) | 1536 | 60.99 | - | +| 12 | [e5-small-v2](https://huggingface.co/intfloat/e5-small-v2) | 384 | 59.93 | 0.13 | +| 32 | [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) | 384 | 56.26 | 0.09 | + +## Benefits of fewer dimensions + +What specifically do we gain when we have fewer dimensions? Faster queries and less RAM: Fewer dimensions means less computation while more of the dataset or index is able to fit in memory. + +Take a look at dot product for example: + +
    + multi database + multi database +
    + +Dot product is the product of each vector element pair summed together into a single result. Fewer dimensions in the vector means fewer calculations for every computed distance . + +We compared the performance of `text-embedding-ada-002` from OpenAI (1536 dimensions) with open-source `all-MiniLM-L6-v2` (384 dimensions) by measuring requests per second at a constant precision and configuration: + +- **Database size:** with 2vCPU (ARM) and 8GB RAM - `large` add-on for Supabase project. +- **Version:** Postgres v15 and pgvector v0.4.0 +- **Dataset:** 224,482 embeddings from [Wikipedia articles](https://huggingface.co/datasets/Supabase/wikipedia-en-embeddings). +- **Index:** The index was generated for `inner-product` (dot product) distance function with `lists=1000`. +- **Process:** We followed [our optimization guide and tips](https://supabase.com/docs/guides/ai/going-to-prod#performance-tips-when-using-indexes). + +We observed pgvector with `all-MiniLM-L6-v2` outperforming `text-embedding-ada-002` by 78% when holding the precision@10 constant at 0.99. This gap increases as you lower the precision. Postgres was using just 4GB of RAM with 384d vectors generated by `all-MiniLM-L6-v2` compared to 7.5GB with `text-embedding-ada-002`. + +
    + multi database + multi database +
    + +
    + multi database + multi database +
    + +After that, we decided to try the recently published [gte-small](https://huggingface.co/thenlper/gte-small) (also 384 dimensions), and the results were even more astonishing. With `gte-small`, we could set `probes=10` to achieve the same level of `precision@10 = 0.99`. Consequently, we observed more than a 200% improvement in requests per second for pgvector with embeddings generated by `gte-small` compared to `all-MiniLM-L6-v2`. + +
    + multi database + multi database +
    + +## Choosing an embedding model + +Ultimately the choice of embedding model will depend on your specific use case and requirements. You would likely want to consider: + +- **Similarity performance:** How well do embeddings match based on the task (MTEB score)? +- **Model size:** What are the operational costs of storing and loading this model into memory? +- **Sequence length:** How many input tokens does the model support? +- **Dimension size:** What is the operational cost of storing and comparing embeddings? +- **Language:** Which written languages do you need to support? Many of the smaller models only support English. + +The goal is to maximize similarity performance and sequence length, while minimizing model size and dimension size. Notably, if we can find a model that performs well while producing as few dimensions as possible, we can immediately improve many of the above challenges developers are facing with pgvector. + +For example [gte-small](https://huggingface.co/thenlper/gte-small), a model recently trained by the [Alibaba DAMO Academy,](https://damo.alibaba.com/) produces only 384 dimensions while ranking higher than `text-embedding-ada-002` on the MTEB leaderboard. + +It's worth noting that many models today are trained on English-only text (such as `gte-small` and `all-MiniLM-L6-v2`). If you need to work with other languages, be sure to choose a model that supports them, such as [multilingual-e5-small](https://huggingface.co/intfloat/multilingual-e5-small). + +## The best of both worlds + +It's important to keep in mind that using an alternative embedding model doesn't preclude using OpenAI for other tasks like text or chat generation. If your application uses embeddings for [retrieval augmented generation](https://supabase.com/docs/guides/ai/examples/nextjs-vector-search) (searching your knowledge base for additional context to pass to a LLM prompt), you can use a lower dimension embedding model for data fetching, then send the plain text to OpenAI's completion models. The retrieval step is completely decoupled from the LLM generation. + +## Why not dimensionality reduction? + +While dimensionality reduction techniques like Principal Component Analysis ([PCA](https://en.wikipedia.org/wiki/Principal_component_analysis)) or t-Distributed Stochastic Neighbor Embedding ([t-SNE](https://en.wikipedia.org/wiki/T-distributed_stochastic_neighbor_embedding)) may seem like a good alternative to trim down the size of text embedding vectors, there is some risk to consider. + +First, these tools may not appropriately model the relationships in high dimensional space. Both PCA and t-SNE are likely to oversimplify the multivariate relationships among different dimensions, leading to loss of important semantic information. This is particularly the case with PCA, which assumes linearity and would fail to preserve nonlinear interactions found in high-dimensional text embeddings. + +Additionally, the validity of the dimensionality reduction models could change over time if the distribution of text data shifts. If the model was trained on older data, it might not accurately reflect or accommodate these shifts. For example, if your company gets a large new customer in the legal field and your embedding dataset contained no legal documents when the dimensionality reduction model was trained, otherwise exploitable structure may be destructively compressed. Both PCA and t-SNE are not inherently adaptable to such changes, which could result in an increasingly poor fit over time. In summary, the derived low-dimensional representations may not maintain semantic coherence of the original high-dimensional vectors. + +## What's next? + +At Supabase we are working on ways to make it easy to generate embeddings using open source, high performance, and low dimension embedding models. Stay tuned for [Launch Week 8](/launch-week) next week! diff --git a/apps/www/_blog/2023-08-04-interactive-constellation-threejs-react-three-fiber.mdx b/apps/www/_blog/2023-08-04-interactive-constellation-threejs-react-three-fiber.mdx new file mode 100644 index 00000000000..29f358a25c8 --- /dev/null +++ b/apps/www/_blog/2023-08-04-interactive-constellation-threejs-react-three-fiber.mdx @@ -0,0 +1,451 @@ +--- +title: Coding the stars - an interactive constellation with Three.js and React Three Fiber +description: How we built a constellation of stars with Three.js and React Three Fiber. +author: fsansalvadore +image: lw8-constellation-breakdown/lw8-constellation-breakdown-og.png +thumb: lw8-constellation-breakdown/lw8-constellation-breakdown-og.png +tags: + - launch-week +date: '2023-08-04' +toc_depth: 3 +--- + +Every Launch Week is an opportunity for Supabase to experiment, try some spicy new designs, and dogfood our own technology. During our previous Launch Week we [took Generative AI for a spin](https://supabase.com/blog/designing-with-ai-midjourney). This time we decided to shoot for the stars. + +For [Launch Week 8](https://supabase.com/launch-week/), we wanted to make the user-generated tickets a central piece of the launch week theme. To do this, we built a “constellation” of stars - an animated night sky where every user signup was represented as a star, in the form of an “8” shape. + + + +We could approach this animation in a few ways. + +For example, animating the `stroke-dashoffset` on an SVG path, similar to example, was a good option, but it would have been difficult to randomize and change the shape at a later stage. Other approaches included 2D animation libraries, like , or . + +Ultimately we decided to take for a spin using [React Three Fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) (R3F) giving us a more powerful toolset to enable us to achieve the best possible result. + +Learning Three.js is not a walk in the park but R3F abstracted many of its complexities such as cameras and renderers, to name a few. If you're new to R3F, some of the core primitives they provide for a basic scene include: + +- `Geometries`: used to create and define shapes +- `Materials`: manage the texture and color of objects +- `Mesh`: used to instantiate polygonal objects by combining a Geometry with a Material +- `Lights`: to shine bright like a diamond 💎🎵 +- `Canvas`: where you define your R3F Scene + +If you want to dive a little deeper, here are a few good resources we found to get a solid grasp on the topic: + +- - from our very own +- - by + +## Setting up the scene + +In this article, we’re going to break down the steps to reproduce the Launch Week 8 animation using React Three Fiber in NextJs. + +These are the dependencies we’ll need: + +```bash +npm install three @react-three/fiber +``` + +If you’re using React 17, we’ll spare you the trouble of finding the last compatible R3F version: + +```bash +npm install three @react-three/fiber@7.0.29 +``` + +All we need for each particle is a circle geometry with a minimal amount of sides to minimize complexity. + +```jsx +import { useMemo } from 'react' + +const Geometry = useMemo(() => () => , []) +``` + +A basic standard material with a white color will do just fine. Using the AdditiveBlending module from `three` provides a more interesting touch when particles happen to overlap, making them shine brighter: + +```jsx +import { AdditiveBlending } from 'three' + +const Material = () => + useMemo(() => , []) +``` + +Let’s put it together in an R3F `Canvas` element and wrap up the initial setup with an `ambientLight`, which will make objects visible, just as real light does: + +```jsx +import { useMemo } from 'react' +import { Canvas } from '@react-three/fiber' +import { AdditiveBlending } from 'three' + +const Geometry = useMemo( + () => () => , + [] + ) + +const Material = () => + useMemo( + () => ( + + ), + [] + ) + +return ( +
    + + + + {particles?.map((particle, index) => ( + + + + + ))} + + +
    +) +``` + +For more context, the `dpr` values help with pixelation issues and the `camera` [0, 0, 500] position means that the camera is moved 500 units back in the z-axis to actually see the center [0,0,0] of the scene. + +One thing to note is that the R3F Canvas renders a transparent background, so in order to see the white particle, we need to set the background of the parent html element to a dark color. + +We created a separate component for the Particle, which will later encapsulate the animation logic. + +```jsx +import React, { useRef } from 'react' + +const Particle = ({ children }) => { + const particle = useRef(null) + + return {children} +} + +export default Particle +``` + +## Load users from Supabase + +You might have noticed we haven’t instantiated the particles yet. As we mentioned earlier, we wanted each particle to represent a ticket generated by a user and stored in the database. Let’s fetch the signups from the `tickets` table in our Supabase project (you might need to start your own Launch Week to fill your table): + +```jsx +const [particles, setParticles] = useState([]) + +const loadUsers = async () => { + return await supabase.from('lw8_tickets').select('*') +} + +useEffect(() => { + const { data: users } = loadUsers() + setParticles(users) +}, []) +``` + +We updated the constellation in [realtime](https://supabase.com/realtime) whenever a new ticket was generated, but we’ll skip over this part to keep the article shorter. Since it’s all open-source, you can dive deeper if you wish. + +## Animating the particles + +To move the particle around the screen we are going to leverage a few different concepts: _useFrame_ and _trigonometry_ 🤯 + +### useFrame + +Generally, the most optimal way to animate things in a browser viewport, using javascript, is by leveraging a method called , which _“tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation right before the next repaint.”_. R3F has a similar hook called that lets you execute code on every frame of Fiber's render loop. We’ll use this to change the position of the particles over time in a few moments. + +### Using time as an animation variable + +We can extract time information from the useFrame `clock` parameter, to know how much time has elapsed in our application, and use that time to animate a value. Updating the `x` position with Math.sin() generates a horizontal oscillating movement. Multiply it with a `widthRadius` variable to customize the amplitude of the movement. + +```jsx +const particle = useRef(null) + +const widthRadius = 100 +const heightRadius = 100 + +useFrame(({ clock }) => { + const timer = clock.getElapsedTime() + + particle.current.position.x = Math.sin(timer) * widthRadius + } +}) + +return {children} +``` + +![01-x-sine.gif](/images/blog/lw8-constellation-breakdown/01-x-sine.gif) + +Combine the previous horizontal movement with a `Math.cos()` on the `y` position to [draw a circle](https://en.wikipedia.org/wiki/Sine_and_cosine): + +```jsx +particle.current.position.y = Math.cos(timer) * heightRadius +``` + +![02-circle.gif](/images/blog/lw8-constellation-breakdown/02-circle.gif) + +Calculating the circumference we can get the time the x position takes to complete a full circle. + +```jsx +const circumference = (config.widthRadius * Math.PI * 2) / 100 +``` + +When that happens, we can invert the cos sign on every other loop to obtain a basic 8 shape. + +```jsx +const isEven = Math.floor(timer / circumference) % 2 == 0 + +particle.current.position.x = Math.sin(timer) * widthRadius +particle.current.position.y = isEven + ? Math.cos(timer) * heightRadius - heightRadius + : -Math.cos(timer) * heightRadius + heightRadius +``` + +![03-eight.gif](/images/blog/lw8-constellation-breakdown/03-eight.gif) + +At this point, we played around with a number of parameters that made the animation more randomized and interesting. + +For example, we randomized the speed and the delay of each particle: + +```jsx +const minSpeed = -0.3 +const maxSpeed = 0.4 +const speed = Math.random() * (minSpeed - maxSpeed) + maxSpeed + +const delayOffsetFactor = 100 +const delayOffset = Math.random() * delayOffsetFactor + +[...] + +const timer = clock.getElapsedTime() *** speed + delayOffset** +``` + +We offset the shape on the x-axis, to concentrate most of the particles in the core of the 8 shape and leave a smaller amount externally, by playing around with exponentials using Math.pow() in combination with some more randomization. + +```jsx +const xThickness = 7 +const xRandomnessShape = 2.2 +const xRandomness = 5 + +const pathOffset = + Math.pow( + Math.random() * xRandomnessShape, + xRandomness - xRandomness / 2 + ) * xThickness + +... + +particle.current.position.x = Math.sin(timer) * widthRadius + pathOffset +``` + +Honestly, this was the result of a lot of playing and tweaking around, and we certainly didn’t hit the best possible result on the first try. Perhaps you want to take some time to experiment with the math - you might find even better and more configurable results. + +## GUI playground + + + +What really helped to visualize the shape, gather feedback, and decide on a final design was adding a GUI to play around with the values. You can try for yourself by appending `#debug` to the [supabase.com/launch-week#debu](http://supabase.com/launch-week#debug)[g](http://supabase.com/launch-weekdebug) url. Go crazy with it. + +We used the library: + +```bash +npm install dat.gui@0.7.9 +``` + +Which needs to be loaded asynchronously, otherwise it raises a `window is not defined` error. + +```jsx +const init = async () => { + const dat = await import('dat.gui') + const gui = new dat.GUI() +} + +useEffect(() => { + init() +}, []) +``` + +Then we prepared a `useParticlesConfig` hook with all the configuration wired up to the GUI. Whenever the GUI updated, we also updated react state. + +```jsx +import { useEffect, useState } from 'react' +import { range } from 'lodash' + +let defaultConfig = { + particles: 1500, + widthRadius: 100, + topHeightRadius: 80, + bottomHeightRadius: 100, + xThickness: 7, + xRandomnessFactor: 2.2, + xRandomnessShape: 2.2, + xRandomness: 5, + yThickness: 20, + max_speed: 0.1, + min_speed: -0.1, +} + +const useParticlesConfig = (): any => { + if (typeof window === 'undefined') return null + const hash = window.location.hash + const isDebugMode = hash.includes('#debug') + const [particles, setParticles] = useState(range(0, defaultConfig.particles)) + + const [config, setConfig] = useState(defaultConfig) + + const handleSetConfig = (name, value) => { + setConfig((prevConfig) => ({ ...prevConfig, [name]: value })) + } + + const init = async () => { + if (!isDebugMode) return + const dat = await import('dat.gui') + const gui = new dat.GUI() + const particlesFolder = gui.addFolder('Particles') + const shapeFolder = gui.addFolder('Shape') + + particlesFolder + .add(config, 'particles') + .min(1) + .max(5000) + .step(1) + .onChange((value) => { + handleSetConfig('particles', value) + setParticles(range(0, value)) + }) + shapeFolder + .add(config, 'widthRadius') + .min(1) + .max(200) + .step(1) + .onChange((value) => handleSetConfig('widthRadius', value)) + + // add desired folders and parameters + + particlesFolder.open() + shapeFolder.open() + } + + useEffect(() => { + init() + }, []) + + return { config, handleSetConfig, particles, setParticles, isDebugMode } +} + +export default useParticlesConfig +``` + +Here is the final code: + +```jsx +import React, { useMemo, useEffect, useState } from 'react' +import { Canvas, useFrame } from '@react-three/fiber' +import { AdditiveBlending } from 'three' +import useParticlesConfig from './hooks/useParticlesConfig' + +const ParticlesCanvas = () => { + if (typeof window === 'undefined') return null + const { config, particles } = useParticlesConfig() + + const Geometry = useMemo( + () => () => , + [] + ) + + const Material = () => + useMemo( + () => ( + + ), + [] + ) + + return ( +
    + + + + {particles?.map((particle, index) => ( + + + + + ))} + + +
    + ) +} + +const Particle = ({ children }: Props) => { + const particle = useRef(null) + + const pathOffset = + Math.pow( + Math.random() * config.xRandomnessShape, + config.xRandomness - config.xRandomness / 2 + ) * config.xThickness + + const verticalRandomness = Math.random() * (config.yThickness - 1) + 1 - config.yThickness / 2 + + const speed = Math.random() * (config.min_speed - config.max_speed) + config.max_speed + + const circumference = (config.widthRadius * Math.PI * 2) / 100 + const delayOffsetFactor = 100 + const delayOffset = Math.random() * delayOffsetFactor + + useFrame(({ clock }) => { + const timer = clock.getElapsedTime() * speed + delayOffset + const isEven = Math.floor(timer / circumference) % 2 == 0 + + // When the loop count is even, draw bottom 8 shape + // if odd, draw top 8 shape + particle.current.position.x = isEven + ? Math.sin(timer) * config.widthRadius * config.widthRatio + pathOffset + : Math.sin(timer) * config.widthRadius + pathOffset + particle.current.position.y = isEven + ? Math.cos(timer) * config.bottomHeightRadius - + config.bottomHeightRadius + + verticalRandomness + : -Math.cos(timer) * config.topHeightRadius + config.topHeightRadius + verticalRandomness + }) + + return {children} +} + +export default Particle +``` + +Now **_THAT’S_** how you create a new constellation ✨. Feel free to use the code and learnings to build your own. + +## Conclusion + +In this journey, you saw how to use Three.js and harness the power of React Three Fiber and creative experimentation to craft an animation. We leveraged trigonometry, animation hooks, and GUI playgrounds to build a "8" shape formed by user-generated stars. + +If you loved this and the new [Launch Week 8](https://supabase.com/launch-week) branding, make sure to tune in on Monday at 09 AM PT as we unveil the full landing 💥 + +## More Supabase Design + +- [Designing with AI: Generating unique artwork for every user](https://supabase.com/blog/designing-with-ai-midjourney) +- **[Infinite scroll with Next.js, Framer Motion, and Supabase](https://supabase.com/blog/infinite-scroll-with-nextjs-framer-motion)** diff --git a/apps/www/_blog/2023-08-05-why-supabase-remote.mdx b/apps/www/_blog/2023-08-05-why-supabase-remote.mdx new file mode 100644 index 00000000000..59ba65308a1 --- /dev/null +++ b/apps/www/_blog/2023-08-05-why-supabase-remote.mdx @@ -0,0 +1,81 @@ +--- +title: "Why we'll stay remote" +description: Offices are making a comeback, just not at Supabase. +author: ant_wilson +image: remote/og.jpg +thumb: remote/thumb.jpg +launchweek: 8 +tags: + - launch-week +date: '2023-08-05' +toc_depth: 2 +--- + +There’s been a major U-turn in people’s attitudes towards remote this year. In 2020-2021 we had the ‘Remote Revelation’ where everyone and their dog realized they can do their job perfectly well from the comfort of their own home (oftentimes in the company of said dog). + +Now in 2023, there’s been whiplash back to the office with larger companies fighting especially hard to convince their employees to return to their desks. + +Supabase started in January 2020, and the plan was always to be fully remote. In some ways COVID gave us a competitive advantage in that we could offer a stable and supportive remote working environment to those whose employers were struggling to adapt. Even now as companies fight for people to go back to the office, we’re attracting a lot of talented people who moved out of major cities and would rather hop somewhere supportive rather than relocate or commute. + +There are good arguments on both sides. And while you might point out that it’s probably too impractical for Supabase to switch to in-person now anyway (we’re already in >25 countries), the argument I’m making here is that if we were starting again tomorrow, we would do it all the same. + +## No Commute + +During my first job out of college I spent an average of 2 hours every day driving to the office and back. This was valuable thinking time, it allowed me to rediscover the entire Weezer back catalog. Being in the days before (good) podcasts it gave me time to listen to the Today programme on Radio 4 and catch up on what was happening in the world. Despite all this however, it struck me as one of the most clearly insane things we force humans to do on a daily basis - spend an average of 20% of their waking hours driving or on public transport. Many people report that the first 2 hours after waking are their most productive; are we really asking them to spend those precious hours commuting? It does make some sense from the companies perspective, because the company doesn’t pay for this time, nor do they usually directly pay for the cost of the commute (which can often take a hefty chunk out of your wage). In exchange they get the benefit of extra productivity from the portion of the population who require a lot of face time in order to get their job done. While I miss listening to Pinkerton on repeat, it turned out there are other ways to get back my thinking/music/podcast time and that is going for a long walk in the woods, or commuting to the gym. + +## Requiring a lot of face time to get your job done + +There’s a large portion of the population who fit this description, and without casting any negative judgement, we have to be open and honest with candidates that if they’re in this camp - they’ll likely be unhappy working at Supabase. In the early days we thought that these people could adapt, or perhaps be able to satisfy their need for face to face interactions outside of work, but on the whole it wasn’t the case. We actively try to reduce the number of meetings happening across the company (both in person and via video call) which increases friction with these folks. By not supporting this group, we’re reducing the size of the population from which we can hire, but overall our hiring pool is still very much larger than the average in-person company. Thanks to the likes of Rippling, Remote, and Deel, we can extend our talent search globally - and it turns out there are a hell of a lot of _extremely_ talented and highly motivated people living outside of New York, London, and San Francisco (don’t worry we take folks from these cities as well!). After all, the goal of hiring should be to source the absolute best person for the job. Statistically speaking it’s unlikely that this person lives within an hour drive to your office. + +## More take home pay + +For the major US cities, we pay somewhere in the 60-80th percentile range, but for almost everywhere else we _easily_ pay in the 95th percentile (in many cases in the 99th percentile) because we don’t do geo-adjusted pay. If you want to live off the beaten path, where the air is clean, and the taxes are low - then that’s entirely up to you. We won’t lower your pay and you’ll end up with much more cash in your pocket at the end of the month. We’re strongly of the opinion that you should get the same pay for the same job, geo-adjusted pay always seemed a bit bizarre to me. It’s undeniable that major cities have a higher concentration of technical talent, but these people are taxed accordingly. If not on their income, then most certainly on their rent and other expenses. + +## Won’t somebody think of the young people + +An argument I often hear for being in the office, is that young people joining the workforce can’t learn the ways of the working world as quickly when working remotely because they don’t get as much exposure to senior members of the team. This may be true. In my experience, young people seem much better adapted to working remotely than people who have been in the workforce for several decades, however it doesn’t solve for the more experienced folks being visible and available for the juniors to learn from. At Supabase, we very rarely hire juniors. That’s not to say we don’t hire young people, since there are plenty of highly skilled young people who can out-ship people twice their age. But in terms of team make-up, our team is very highly skewed towards seniors and leads. So we don’t really suffer much from this issue (if it exists). + +## Global Coverage + +Supabase now has team members in more than 25 countries. One benefit of this as an infrastructure provider is that we get 24 hour support coverage for free. If your database is showing signs of trouble at 3am, you don’t need to worry because we have team members on the clock and actively responding to system alerts. It also means that the shipping quite literally never stops. + +When interviewing candidates a common question we get is around working hours, and the honest answer is that we don’t care when you work. With the number of different timezones we operate across it would be impossible for us to track anyway. Some people prefer to start their day at 5am, and others prefer to work late into the night. That’s fine by us. + +Most of the people we hire have a background in open source and contribute to large projects like PostgreSQL where most things run asynchronously. They are well adapted to working on complex designs independently. It seems that this style of open source is becoming more prevalent thanks in part to the meteoric success of GitHub. As a consequence, the pool of talent is undoubtedly growing. + +Another huge benefit of hiring from anywhere is that you end up with an incredibly diverse team. This helps when selling to a global audience of developers, and ensures diversity of thought and a broad range of perspectives. + +## Won’t somebody think of the commercial real-estate investors + +A huge and obvious advantage of not having an office… is not having to pay for an office. Dedicated offices can get pretty expensive, so I’m not surprised large companies resent sitting on acres of unused office space. But fear not, here at Supabase we’ve uncovered an ingenious solution to keeping team members happy and at the same time supporting our local real estate developers. We give everyone the option of a desk at a local co-working space. In cities where we have many employees they tend to opt for the WeWork floating desk. They can also access WeWork locations globally when they travel (other co-working spaces are available). This adds some operating costs, but it’s far more affordable than a dedicated space, and a lot of people also choose not to take us up on the offer and prefer to work from their kitted out spaces at home, a local coffee shop, or a Brazilian beach resort. + +## The Human Connection + +When you save so much on office space it extends the budget for whole company off-sites (on-sites?). At the end of the day we’re all human, and we know the importance of getting to know your team mates on a human level. That’s why once per year we get everyone together for 11 days. This year we went to Bali. + +On top of whole company trips, folks often self organize co-working trips to places like Malaysia, San Francisco, or Austin, Texas which helps with getting to know folks outside of your immediate team. + +## Productivity + +After all is said and done, companies will still ask “but where will my team be most productive?”. To be honest I think the Supabase team would be just as productive in an office as they would be from home, but would we have been able to hire these same folks without a remote working policy? Absolutely not. + +A lot of people’s hesitations around remote seem to stem from not having trust in their team to get things done without adult supervision. However, if I know anything about high performing teams, it’s that you need to have mutual trust (which breeds psychological safety), and a shared model of what success looks like (metrics anyone?). Without these you’re going to struggle to produce results office or no office. + +At Supabase we do what works for Supabase, and for many companies being in a dedicated office or hybrid will undoubtably be what’s best for them. + +The book that shaped my thinking on remote more than any other was Spolsky’s ‘Smart and Gets Things Done’ (I don’t even know if it’s in print any more?), but the summary is this: + +Hire people who are: + +- Smart +- Get Things Done + +Everything else is a Nice to Have. Including proximity. + +Lastly, if you hate wasting time commuting and want to work with some of the best engineers in the world, [we’re hiring](https://supabase.com/careers). + +Read more: + +- [Who we hire at Supabase](https://supabase.com/blog/who-we-hire) +- [Supabase Careers](https://supabase.com/careers) +- [See what's new in Supabase](https://supabase.com/launch-week) diff --git a/apps/www/components/AuthWidget/AuthWidgetSection.tsx b/apps/www/components/AuthWidget/AuthWidgetSection.tsx index f9ee1bbf25a..6d840b80c68 100644 --- a/apps/www/components/AuthWidget/AuthWidgetSection.tsx +++ b/apps/www/components/AuthWidget/AuthWidgetSection.tsx @@ -19,9 +19,9 @@ function AuthWidgetSection() { const { basePath } = useRouter() const [radius, setRadius] = useState('4px') const [brandColor, setBrandColor] = useState({ - brand: 'var(--colors-brand9)', - brandAccent: 'var(--colors-brand11)', - brandAccent2: 'var(--colors-brand3)', + brand: 'hsl(var(--brand-default))', + brandAccent: 'hsl(var(--brand-500))', + brandAccent2: 'hsl(var(--brand-200))', }) const [layout, setLayout] = useState('horizontal') @@ -140,15 +140,15 @@ function AuthWidgetSection() { diff --git a/apps/www/components/Button.tsx b/apps/www/components/Button.tsx index babd6994b7f..549409500a4 100644 --- a/apps/www/components/Button.tsx +++ b/apps/www/components/Button.tsx @@ -13,8 +13,8 @@ const Button = (props: Props) => { const colorClass = type === 'primary' - ? 'px-3 py-2 shadow-sm border border-transparent text-white bg-brand-700 hover:bg-brand-800 focus:ring-2 focus:ring-offset-2 focus:ring-brand-500' - : 'text-brand-700 bg-none' + ? 'px-3 py-2 shadow-sm border border-transparent text-white bg-brand-400 hover:bg-brand-300 focus:ring-2 focus:ring-offset-2 focus:ring-brand-300' + : 'text-brand-400 bg-none' const textClass = type === 'primary' ? 'font-medium left-3 group-hover:left-0' : 'font-normal' diff --git a/apps/www/components/Charts/PGCharts.tsx b/apps/www/components/Charts/PGCharts.tsx index f73e0454b2f..982c1097fc9 100644 --- a/apps/www/components/Charts/PGCharts.tsx +++ b/apps/www/components/Charts/PGCharts.tsx @@ -7,7 +7,7 @@ import * as data from '~/data/blog/PostgresFTSComparison' import remarkGfm from 'remark-gfm' const barColors = { - pg: 'var(--colors-brand11)', + pg: 'hsl(var(--brand-600))', meilisearch: 'var(--colors-blue11)', typesense: 'var(--colors-violet11)', 'sqlite-disk': 'var(--colors-orange11)', diff --git a/apps/www/components/CodeBlock/CodeBlock.tsx b/apps/www/components/CodeBlock/CodeBlock.tsx index 99f0cb02169..1269b1f11ff 100644 --- a/apps/www/components/CodeBlock/CodeBlock.tsx +++ b/apps/www/components/CodeBlock/CodeBlock.tsx @@ -119,7 +119,7 @@ function CodeBlock(props: CodeBlockProps) { type="text" icon={ copied ? ( - + ) : ( diff --git a/apps/www/components/Hero/Hero.tsx b/apps/www/components/Hero/Hero.tsx index 6aba7c1539c..9ce53bff3a8 100644 --- a/apps/www/components/Hero/Hero.tsx +++ b/apps/www/components/Hero/Hero.tsx @@ -41,7 +41,7 @@ const Hero = () => { {showAnnouncement && ( -
    +
    Get your ticket
    Launch Week 8 - August 7-11 diff --git a/apps/www/components/InteractiveShimmerCard/index.tsx b/apps/www/components/InteractiveShimmerCard/index.tsx index 3e11c07bd46..d6017544f96 100644 --- a/apps/www/components/InteractiveShimmerCard/index.tsx +++ b/apps/www/components/InteractiveShimmerCard/index.tsx @@ -34,9 +34,9 @@ const InteractiveShimmerCard = ({ const isActive = x > -3 && x < width + 3 && y > -3 && y < height + 3 const activeGlow = hasActiveOnHover && isActive - ? `radial-gradient(65rem circle at ${x}px ${y}px, var(--colors-brand9), transparent), ` + ? `radial-gradient(65rem circle at ${x}px ${y}px, hsl(var(--brand-default)), transparent), ` : '' - outerElement.style.background = isActive ? `var(--colors-brand9)` : `var(--colors-scale3)` + outerElement.style.background = isActive ? `hsl(var(--brand-default))` : `var(--colors-scale3)` outerElement.style.backgroundImage = ` ${activeGlow}radial-gradient(30rem circle at ${x}px ${y}px, ${ shimmerFromColor ?? 'var(--colors-scale8)' diff --git a/apps/www/components/LaunchWeek/6/HackathonSection.tsx b/apps/www/components/LaunchWeek/6/HackathonSection.tsx index ad73a8a52a5..c2be728bb50 100644 --- a/apps/www/components/LaunchWeek/6/HackathonSection.tsx +++ b/apps/www/components/LaunchWeek/6/HackathonSection.tsx @@ -44,7 +44,7 @@ export default function LaunchHero() {

    Submission

    Submit your project through{' '} - + madewithsupabase.com . diff --git a/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButtonListItem.tsx b/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButtonListItem.tsx index e374bd023fc..8aee78e4070 100644 --- a/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButtonListItem.tsx +++ b/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButtonListItem.tsx @@ -9,7 +9,7 @@ const ArticleButtonListItem = (props: Article) => {

    - + {props.title}

    {props.description}

    diff --git a/apps/www/components/LaunchWeek/7/LaunchSection/ProductButton.tsx b/apps/www/components/LaunchWeek/7/LaunchSection/ProductButton.tsx index 550d0e28815..14fa872e674 100644 --- a/apps/www/components/LaunchWeek/7/LaunchSection/ProductButton.tsx +++ b/apps/www/components/LaunchWeek/7/LaunchSection/ProductButton.tsx @@ -6,7 +6,7 @@ const ProductButton = (props: Article) => {
    diff --git a/apps/www/components/LaunchWeek/7/LaunchSection/index.tsx b/apps/www/components/LaunchWeek/7/LaunchSection/index.tsx index 3faf36f20d0..db093a84ca8 100644 --- a/apps/www/components/LaunchWeek/7/LaunchSection/index.tsx +++ b/apps/www/components/LaunchWeek/7/LaunchSection/index.tsx @@ -25,7 +25,7 @@ export const LaunchSection = (props: WeekDayProps) => {
    {/* END timeline dot */} @@ -63,7 +63,7 @@ export const LaunchSection = (props: WeekDayProps) => { nextDayNotShipped ? 'launch-week-timeline-border--approaching' : props.shipped - ? 'border-brand-900' + ? 'border-brand' : 'border-purple-700', className, ].join(' ')} @@ -152,7 +152,7 @@ export const LaunchSection = (props: WeekDayProps) => { md:pl-8 lg:pl-8 `, - props.shipped ? 'border-brand-900' : 'border-purple-700', + props.shipped ? 'border-brand' : 'border-purple-700', props.shippingHasStarted && 'lg:col-span-6', !lastIndex && 'pb-8 lg:pb-16', ].join(' ')} diff --git a/apps/www/components/LaunchWeek/7/LaunchWeekPrizeSection.tsx b/apps/www/components/LaunchWeek/7/LaunchWeekPrizeSection.tsx index 4f8d365192b..c469dac9aa8 100644 --- a/apps/www/components/LaunchWeek/7/LaunchWeekPrizeSection.tsx +++ b/apps/www/components/LaunchWeek/7/LaunchWeekPrizeSection.tsx @@ -54,13 +54,8 @@ export default function LaunchWeekPrizeSection({

    - Get your winning ticket + Submissions closed

    -

    - Join us on April 16th for Launch Week 7's final day -
    and find out if you are one of the lucky - winners. Get sharing! -

    {ticket && } diff --git a/apps/www/components/LaunchWeek/7/Ticket/form.module.css b/apps/www/components/LaunchWeek/7/Ticket/form.module.css index 5b6a078561b..1bdf317bbc9 100644 --- a/apps/www/components/LaunchWeek/7/Ticket/form.module.css +++ b/apps/www/components/LaunchWeek/7/Ticket/form.module.css @@ -130,8 +130,8 @@ height: 56px; margin-top: var(--space-4x); border-radius: var(--space-2x); - border: 1px solid var(--brand); - background: var(--brand); + border: 1px solid hsl(var(--brand-default)); + background: hsl(var(--brand-default)); cursor: pointer; font-family: inherit; font-size: var(--text-md); @@ -170,7 +170,7 @@ .submit.default:hover, .submit.default:focus { background: black; - color: var(--brand); + color: hsl(var(--brand-default)); } .submit.default.generate-with-github:hover path, diff --git a/apps/www/components/LaunchWeek/8/AnimatedParticles/ParticlesCanvas.tsx b/apps/www/components/LaunchWeek/8/AnimatedParticles/ParticlesCanvas.tsx index 6d88c89268a..d30c96261f5 100644 --- a/apps/www/components/LaunchWeek/8/AnimatedParticles/ParticlesCanvas.tsx +++ b/apps/www/components/LaunchWeek/8/AnimatedParticles/ParticlesCanvas.tsx @@ -3,56 +3,18 @@ import { Canvas } from '@react-three/fiber' import { AdditiveBlending } from 'three' import Particle from './Particle' import useParticlesConfig from './hooks/useParticlesConfig' -import { SupabaseClient } from '@supabase/supabase-js' -const ParticlesCanvas = ({ supabase, users }: { supabase?: SupabaseClient; users: any }) => { +const ParticlesCanvas = () => { const isWindowUndefined = typeof window === 'undefined' if (isWindowUndefined) return null const canvasRef = React.useRef(null) const [animate, setAnimate] = useState(true) - const { config, handleSetConfig, particles, setParticles, isDebugMode } = - useParticlesConfig(users) - const [realtimeChannel, setRealtimeChannel] = useState | null>(null) - - const loadUsers = async () => { - return await supabase!.from('lw8_tickets_golden').select('username, golden', { count: 'exact' }) - } - - // Update particles live when new tickets are generated - useEffect(() => { - if (!!supabase) { - const channel = supabase - .channel('lw8_tickets_changes') - .on( - 'postgres_changes', - { - event: 'INSERT', - schema: 'public', - table: 'lw8_tickets_golden', - }, - async () => { - const { data: users } = await loadUsers() - - handleSetConfig('particles', users?.length) - setParticles((prev: any) => users! ?? prev) - } - ) - .subscribe() - setRealtimeChannel(channel) - } - - return () => { - // Cleanup realtime subscription on unmount - realtimeChannel?.unsubscribe() - } - }, []) + const { config, particles } = useParticlesConfig() // stop animation if canvas if is not in viewport - // to avoid unnecessary computations + // to stop unnecessary computations const handleScroll = () => { if (canvasRef.current && typeof window !== 'undefined') { const rect = (canvasRef.current as HTMLDivElement)?.getBoundingClientRect() @@ -83,7 +45,7 @@ const ParticlesCanvas = ({ supabase, users }: { supabase?: SupabaseClient; users const Material = () => useMemo( () => ( - @@ -110,8 +72,7 @@ const ParticlesCanvas = ({ supabase, users }: { supabase?: SupabaseClient; users className="relative z-30" > - - {/* Animated 8 shape particles */} + {particles?.map((user: any, index: number) => ( { +const useParticlesConfig = (): any => { const isWindowUndefined = typeof window === 'undefined' if (isWindowUndefined) return null const hash = window.location.hash const isDebugMode = hash.includes('#debug') - const [particles, setParticles] = useState(users) + const [particles, setParticles] = useState(range(0, defaultConfig.particles)) const [config, setConfig] = useState(defaultConfig) @@ -48,7 +44,7 @@ const useParticlesConfig = (users: any): any => { const particlesFolder = gui.addFolder('Particles') const shapeFolder = gui.addFolder('Shape') const speedFolder = gui.addFolder('Speed') - // const trailFolder = gui.addFolder('Trail') + gui.width = 500 particlesFolder .add(config, 'particles') diff --git a/apps/www/components/LaunchWeek/8/Countdown.tsx b/apps/www/components/LaunchWeek/8/Countdown.tsx index 322b7a944db..e8394b6128f 100644 --- a/apps/www/components/LaunchWeek/8/Countdown.tsx +++ b/apps/www/components/LaunchWeek/8/Countdown.tsx @@ -1,20 +1,35 @@ import React from 'react' import Countdown from 'react-countdown' import { CountdownWidget } from 'ui' -import { LW8_LAUNCH_DATE } from '~/lib/constants' -const CountdownComponent = () => { +const CountdownComponent = ({ + date, + showCard = true, +}: { + date: string | number | Date + showCard?: boolean +}) => { + if (!date) return null + const renderer = ({ days, hours, minutes, seconds, completed }: any) => { if (completed) { // Render a completed state return null } else { // Render countdown - return + return ( + + ) } } - return + return } export default CountdownComponent diff --git a/apps/www/components/LaunchWeek/8/LW8CalloutsSection.tsx b/apps/www/components/LaunchWeek/8/LW8CalloutsSection.tsx new file mode 100644 index 00000000000..2183e0b6df6 --- /dev/null +++ b/apps/www/components/LaunchWeek/8/LW8CalloutsSection.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import SectionContainer from '../../Layouts/SectionContainer' +import { SmallCard } from '../8/Releases/components' +import Link from 'next/link' +import Image from 'next/image' + +const LW8CalloutsSection = () => { + return ( + +
    + + + +
    +
    + Join us daily at Twitter Spaces + Next up: Monday – Opening Day +
    +
    +
    + twitter spaces thumbnail +
    +
    + +
    + + + +
    + + + ) +} + +export default LW8CalloutsSection diff --git a/apps/www/components/LaunchWeek/8/LW8Meetups.tsx b/apps/www/components/LaunchWeek/8/LW8Meetups.tsx new file mode 100644 index 00000000000..4a7a8a3e7db --- /dev/null +++ b/apps/www/components/LaunchWeek/8/LW8Meetups.tsx @@ -0,0 +1,105 @@ +import Link from 'next/link' +import React, { useEffect, useState } from 'react' +import { Badge } from 'ui' +import useConfData from '../hooks/use-conf-data' + +export interface Meetup { + id?: any + title: string + isLive: boolean + link: string + display_info: string + start_at: string +} + +const LW8Meetups = ({ meetups }: { meetups?: Meetup[] }) => { + const { supabase } = useConfData() + const [meets, setMeets] = useState(meetups ?? []) + const [realtimeChannel, setRealtimeChannel] = useState | null>(null) + + useEffect(() => { + // Listen to realtime changes + if (supabase && !realtimeChannel) { + const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'lw8_meetups', + }, + async () => { + const { data: newMeets } = await supabase.from('lw8_meetups').select('*') + setMeets(newMeets) + } + ) + .subscribe() + setRealtimeChannel(channel) + } + + return () => { + // Cleanup realtime subscription on unmount + realtimeChannel?.unsubscribe() + } + }, []) + + return ( +
    +
    +

    Join the community

    +

    + Celebrate LW8 at our first-ever live community meetups across various locations. Join the + fun, grab some swag, and meet the Supabase community. +

    +
    +
    + {meets && + meets + ?.sort((a, b) => (new Date(a.start_at) > new Date(b.start_at) ? 1 : -1)) + .map(({ display_info, link, isLive, title }: Meetup) => ( + + +
    + {title} + {isLive && Live now} + + + + + +
    + {display_info && ( + {display_info} + )} +
    + + ))} +
    +
    + ) +} + +export default LW8Meetups diff --git a/apps/www/components/LaunchWeek/8/LWArchive.tsx b/apps/www/components/LaunchWeek/8/LWArchive.tsx new file mode 100644 index 00000000000..3456a49ebfc --- /dev/null +++ b/apps/www/components/LaunchWeek/8/LWArchive.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import { StyledArticleBadge } from './Releases/components' +import Image from 'next/image' +import Link from 'next/link' + +import { Swiper, SwiperSlide } from 'swiper/react' + +import 'swiper/swiper.min.css' + +const launchweeks = [ + { title: '7', link: '/launchweek/7' }, + { title: '6', link: '/launchweek/6' }, + { title: '5', link: '/blog/launch-week-5-hackathon' }, + { title: '4', link: '/blog/supabase-launch-week-four' }, + { title: '3', link: '/blog/supabase-launch-week-the-trilogy' }, + { title: '2', link: '/blog/supabase-launch-week-sql' }, + { title: '1', link: '/blog/launch-week' }, +] + +const LWArchive = () => { + return ( +
    +
    + Throwback +

    Previous Launch Weeks

    +

    + Explore what has been announced in the past and relive those moments. +

    +
    +
    + + {launchweeks.map((launchweek, i) => ( + + + + + {launchweek.title} + + + + + ))} + +
    +
    + ) +} + +export default LWArchive diff --git a/apps/www/components/LaunchWeek/8/LaunchWeekLogoHeader.tsx b/apps/www/components/LaunchWeek/8/LaunchWeekLogoHeader.tsx index 4c0786c62c3..cf61bb72778 100644 --- a/apps/www/components/LaunchWeek/8/LaunchWeekLogoHeader.tsx +++ b/apps/www/components/LaunchWeek/8/LaunchWeekLogoHeader.tsx @@ -1,12 +1,13 @@ import Link from 'next/link' import CountdownComponent from './Countdown' +import { LW8_LAUNCH_DATE } from '../../../lib/constants' export function LaunchWeekLogoHeader() { return (
    - +

    Launch week 8

    diff --git a/apps/www/components/LaunchWeek/8/Releases/components/index.tsx b/apps/www/components/LaunchWeek/8/Releases/components/index.tsx new file mode 100644 index 00000000000..53307c2cf42 --- /dev/null +++ b/apps/www/components/LaunchWeek/8/Releases/components/index.tsx @@ -0,0 +1,347 @@ +import { Badge } from 'ui' + +import Link from 'next/link' +import CountdownComponent from '../../Countdown' + +export const PencilSvg = () => ( + + + +) +export const DocsSvg = () => ( + + + +) +export const PlaySvg = () => ( + + + +) +export const GithubSvg = () => ( + + + +) +export const LinkSvg = () => ( + + + +) +export const ArrowTopRightSvg = () => ( + + + + +) +export const HackernewsSvg = () => ( + + + +) + +export const SmallCard = ({ className, children }: { className?: string; children: any }) => ( +

    +
    + {children} +
    +
    +) + +export const StyledArticleBadge = ({ + className, + children, +}: { + className?: string + children: any +}) => ( +
    +
    + + {children} + +
    +
    +
    +) + +export const AccordionHeader = ({ + date, + day, + weekDay, + title, + shipped, + publishedAt, + shippable = true, +}: { + date: string + day: number + weekDay: string + title: string + shipped?: boolean + publishedAt: string + shippable?: boolean +}) => ( +
    +
    + + {weekDay} + {date && ( + + ・ {date} + + )} + +
    + {shippable && shipped && ( + + + Shipped + + + )} + {shipped && {title}} + {shippable && !shipped && ( + + + + + + + + {day === 1 && } + + )} +
    +) + +export const MultistepSectionHeader = ({ title, blog }: any) => { + return ( +
    +
    + {title && {title}} + {!!blog && ( + + Blog post +
    + +
    +
    + )} +
    +
    + ) +} + +export const ChipLink = ({ + href, + uiOnly, + className, + target, + children, +}: { + href?: string + className?: string + uiOnly?: boolean + target?: '_blank' | '_self' | '_parent' | '_top' | 'framename' + children: any +}) => + uiOnly ? ( + + {children} + + ) : !!href ? ( + + + {children} + + + ) : null + +export const SectionButtons = ({ + blog, + docs, + video, + github, + url, + hackernews, + mobileGrid, +}: { + blog?: string + docs?: string + video?: string + github?: string + url?: string + hackernews?: string + mobileGrid?: boolean +}) => { + return ( +
    + {!!blog && ( + + Blog post +
    + +
    +
    + )} + {!!docs && ( + + Docs +
    + +
    +
    + )} + {!!video && ( + + Video +
    + +
    +
    + )} + {!!github && ( + + Github +
    + +
    +
    + )} + {!!url && ( + + Read +
    + +
    +
    + )} + {hackernews && ( + + Hackernews +
    + +
    +
    + )} +
    + ) +} + +export const CartTitle = ({ children, className }: { children: any; className?: string }) => ( + + {children} +
    + +) + +export default { + AccordionHeader, + DocsSvg, + PencilSvg, + PlaySvg, + CartTitle, + ChipLink, + SectionButtons, + SmallCard, + StyledArticleBadge, +} diff --git a/apps/www/components/LaunchWeek/8/Releases/index.tsx b/apps/www/components/LaunchWeek/8/Releases/index.tsx new file mode 100644 index 00000000000..e8392f56d30 --- /dev/null +++ b/apps/www/components/LaunchWeek/8/Releases/index.tsx @@ -0,0 +1,283 @@ +import Image from 'next/image' +import { motion } from 'framer-motion' +import { Accordion } from 'ui' +import { useBreakpoint } from 'common/hooks/useBreakpoint' + +import days, { WeekDayProps } from './lw8_data' +import SectionContainer from '~/components/Layouts/SectionContainer' +import { AccordionHeader, CartTitle, SectionButtons, StyledArticleBadge } from './components' + +export const defaultEase = [0.25, 0.25, 0, 1] +export const defaultDuratonIn = 0.25 +export const defaultDuratonOut = 0.1 + +export const opacityVariant = { + default: { opacity: 0.9, ease: defaultEase, duration: defaultDuratonOut }, + hover: { + opacity: 1, + transition: { + duration: defaultDuratonIn, + ease: defaultEase, + }, + }, +} +export const opacityVariant2 = { + default: { opacity: 0, ease: defaultEase, duration: defaultDuratonOut }, + hover: { + opacity: 0.2, + transition: { + duration: defaultDuratonIn, + ease: defaultEase, + }, + }, +} +export const opacityVariant3 = { + default: { opacity: 0, ease: defaultEase, duration: defaultDuratonOut }, + hover: { + opacity: 1, + transition: { + duration: defaultDuratonIn, + ease: defaultEase, + }, + }, +} +export const opacityVariant4 = { + default: { opacity: 0.3, ease: defaultEase, duration: defaultDuratonOut }, + hover: { + opacity: 1, + transition: { + duration: defaultDuratonIn, + ease: defaultEase, + }, + }, +} +export const scaleOpacityVariant = { + default: { scale: 1, opacity: 0.9, ease: defaultEase, duration: defaultDuratonOut }, + hover: { + scale: 1.05, + opacity: 1, + transition: { + duration: defaultDuratonIn, + ease: defaultEase, + }, + }, +} +export const scaleOpacityVariant2 = { + default: { scale: 1, opacity: 0.8, ease: defaultEase, duration: defaultDuratonOut }, + hover: { + scale: 1.025, + opacity: 1, + transition: { + duration: defaultDuratonIn, + ease: defaultEase, + }, + }, +} +export const aiImageMobileVariant = { + default: { scale: 0.9 }, + hover: { + scale: 0.9, + }, +} +export const moveX10 = { + default: { x: 0, ease: defaultEase, duration: defaultDuratonOut }, + hover: { + x: 10, + transition: { + duration: defaultDuratonIn, + ease: defaultEase, + }, + }, +} + +export default function LW8Releases() { + const [preRelease, day1, day2, day3, day4, day5] = days + const isTablet = useBreakpoint(1023) + const isDesktop = useBreakpoint(1280) + const showAll = false + const publishedSections = + days + .filter( + (day: WeekDayProps) => day.d === 0 || Date.parse(day.publishedAt) <= Date.now() || showAll + ) + .map((day: WeekDayProps) => day.d.toString()) ?? [] + + const day1Shipped = Date.parse(day1.publishedAt) <= Date.now() + const day2Shipped = Date.parse(day2.publishedAt) <= Date.now() + const day3Shipped = Date.parse(day3.publishedAt) <= Date.now() + const day4Shipped = Date.parse(day4.publishedAt) <= Date.now() + const day5Shipped = Date.parse(day5.publishedAt) <= Date.now() + + return ( + <> + + +
    + + } + key={preRelease.dd} + className="h-[79px]" + id={preRelease.d.toString()} + > + {preRelease.steps.length > 0 && ( +
    + +
    +
    + {preRelease.steps[0]?.title} +

    {preRelease.steps[0]?.description}

    +
    + + {preRelease.steps[0]?.bg_layers && + preRelease.steps[0]?.bg_layers?.map( + (layer, i) => + !!layer.img && ( + + + + ) + )} + +
    + )} + +
    +
    + + } + disabled={!day1Shipped} + className="h-[79px]" + id={day1.d.toString()} + > +
    +
    + + } + disabled={!day2Shipped} + className="h-[79px]" + id={day2.d.toString()} + > +
    +
    + + } + disabled={!day3Shipped} + className="h-[79px]" + id={day3.d.toString()} + > +
    +
    + + } + disabled={!day4Shipped} + className="h-[79px]" + id={day4.d.toString()} + > +
    +
    + + } + disabled={!day5Shipped} + className="h-[79px]" + id={day5.d.toString()} + > +
    + + + + ) +} diff --git a/apps/www/components/LaunchWeek/8/Releases/lw8_data.ts b/apps/www/components/LaunchWeek/8/Releases/lw8_data.ts new file mode 100644 index 00000000000..21381b8b947 --- /dev/null +++ b/apps/www/components/LaunchWeek/8/Releases/lw8_data.ts @@ -0,0 +1,135 @@ +const images = { + '00-constellation': '/images/launchweek/8/day0/constellation-pre.svg', + '00-constellation-mobile': '/images/launchweek/8/day0/constellation.svg', +} + +export interface StepProps { + title: string + badge?: string + break_thumb_title?: boolean + blog?: string + docs?: string + description?: string + github?: string + hackernews?: string + isNew?: boolean + thumb?: string + url?: string + video?: string + className?: string + hideInBlog?: boolean + bg_layers?: { + lottie?: any + img?: string + mobileImg?: string + }[] + steps: StepProps[] | [] +} +export interface WeekDayProps { + title: string + shipped: boolean + date: string + publishedAt: string + description: string + d: number + dd: string + youtube_id: string + blogpost: string + docs: string + steps: StepProps[] | [] +} + +export const endOfLW8 = '2023-08-11T23:59:59.999-07:00' + +const days: WeekDayProps[] = [ + { + title: '', + shipped: true, + date: '04 Aug', + publishedAt: '2023-08-04T09:00:00.000-07:00', + description: '', + d: 0, + dd: 'Pre-release', + youtube_id: '', + blogpost: '', + docs: '', + steps: [ + { + title: + 'Coding the stars - an interactive constellation with Three.js and React Three Fiber', + blog: '/blog/interactive-constellation-threejs-react-three-fiber', + bg_layers: [ + { img: images['00-constellation'], mobileImg: images['00-constellation-mobile'] }, + ], + steps: [], + }, + ], + }, + { + title: '', + shipped: false, + date: '07 Aug', + publishedAt: '2023-08-07T09:00:00.000-07:00', + description: '', + d: 1, + dd: 'Mon', + youtube_id: '', + blogpost: '', + docs: '', + steps: [], + }, + { + title: '', + shipped: false, + date: '08 Aug', + publishedAt: '2023-08-08T09:00:00.000-07:00', + description: '', + d: 2, + dd: 'Tue', + youtube_id: 'cPGxPl1lx4Y', + blogpost: '', + docs: '', + steps: [], + }, + { + title: '', + shipped: false, + date: '09 Aug', + publishedAt: '2023-08-09T09:00:00.000-07:00', + description: '', + d: 3, + dd: 'Wed', + youtube_id: '', + blogpost: '', + docs: '/docs/guides/storage/uploads#resumable-upload', + steps: [], + }, + { + title: '', + shipped: false, + date: '10 Aug', + publishedAt: '2023-08-10T09:00:00.000-07:00', + description: '', + d: 4, + dd: 'Thu', + youtube_id: '', + blogpost: '', + docs: '', + steps: [], + }, + { + title: '', + shipped: false, + date: '11 Aug', + publishedAt: '2023-08-11T00:00:00.000-07:00', + description: '', + d: 5, + dd: 'Fri', + youtube_id: '', + blogpost: '', + docs: '', + steps: [], + }, +] + +export default days diff --git a/apps/www/components/LaunchWeek/8/Releases/styles/launchWeek7.module.css b/apps/www/components/LaunchWeek/8/Releases/styles/launchWeek7.module.css new file mode 100644 index 00000000000..7a1417601cd --- /dev/null +++ b/apps/www/components/LaunchWeek/8/Releases/styles/launchWeek7.module.css @@ -0,0 +1,29 @@ +.mask { + mask-image: radial-gradient(black, transparent); +} + +.dark_community { + background-image: radial-gradient(closest-side at 50% 50%, #132121, #132121db, #121212); +} + +.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; + } +} + +.lw7-article-card-gradient { + background: linear-gradient(90deg, #1c1c1c -20%, #20113d50 150%); +} diff --git a/apps/www/components/LaunchWeek/8/Releases/styles/utils7.module.css b/apps/www/components/LaunchWeek/8/Releases/styles/utils7.module.css new file mode 100644 index 00000000000..c4aba2ab686 --- /dev/null +++ b/apps/www/components/LaunchWeek/8/Releases/styles/utils7.module.css @@ -0,0 +1,76 @@ +.appear { + opacity: 0; + animation: appear 0.8s cubic-bezier(0.1, 0, 0.175, 1) forwards; +} + +.appear-first { + animation-delay: 0.4s; +} + +.appear-second { + animation-delay: 0.8s; +} + +.appear-third { + animation-delay: 1.2s; +} + +.appear-fourth { + animation-delay: 1.6s; +} + +.appear-fifth { + animation-delay: 2s; +} + +.appear-sixth { + animation-delay: 2.4s; +} + +@keyframes appear { + to { + opacity: 1; + } +} + +@media (min-width: 1200px) { + .hide-on-desktop { + display: none; + } +} + +.show-on-mobile { + display: block; +} + +.hide-on-mobile { + display: none; +} + +@media (min-width: 768px) { + .show-on-mobile { + display: none; + } + + .hide-on-mobile { + display: block; + } +} + +.hide-on-tablet { + display: block; +} + +.show-on-tablet { + display: none; +} + +@media (max-width: 1199px) and (min-width: 768px) { + .show-on-tablet { + display: block; + } + + .hide-on-tablet { + display: none; + } +} diff --git a/apps/www/components/LaunchWeek/8/Ticket/TicketActions.tsx b/apps/www/components/LaunchWeek/8/Ticket/TicketActions.tsx index 3a81766bc57..103e8b5e63b 100644 --- a/apps/www/components/LaunchWeek/8/Ticket/TicketActions.tsx +++ b/apps/www/components/LaunchWeek/8/Ticket/TicketActions.tsx @@ -20,8 +20,8 @@ export default function TicketActions({ ticketGenerationState, setTicketGenerationState, }: Props) { - const [imgReady, setImgReady] = useState(false) - const [loading, setLoading] = useState(false) + const [_imgReady, setImgReady] = useState(false) + const [_loading, setLoading] = useState(false) const downloadLink = useRef() const link = `${SITE_URL}/tickets/${username}?lw=8${golden ? `&golden=true` : ''}` const permalink = encodeURIComponent(link) diff --git a/apps/www/components/LaunchWeek/8/Ticket/TicketCustomizationForm.tsx b/apps/www/components/LaunchWeek/8/Ticket/TicketCustomizationForm.tsx index dd9aa9e243c..6d6f848b536 100644 --- a/apps/www/components/LaunchWeek/8/Ticket/TicketCustomizationForm.tsx +++ b/apps/www/components/LaunchWeek/8/Ticket/TicketCustomizationForm.tsx @@ -54,9 +54,7 @@ const TicketCustomizationForm = ({ supabase, user }: Props) => { Connected account )} - {IS_SAVED && ( - Saved - )} + {IS_SAVED && Saved} {HAS_ERROR && ( Something went wrong @@ -72,7 +70,7 @@ const TicketCustomizationForm = ({ supabase, user }: Props) => { disabled placeholder="name" value={user.name} - icon={} + icon={} /> { className={[ 'w-3', IS_SAVING && 'text-scale-300', - !!formData.role ? 'text-brand-900' : 'text-scale-300', + !!formData.role ? 'text-brand' : 'text-scale-300', ].join(' ')} /> } @@ -113,7 +111,7 @@ const TicketCustomizationForm = ({ supabase, user }: Props) => { className={[ 'w-3', IS_SAVING && 'text-scale-300', - !!formData.company ? 'text-brand-900' : 'text-scale-300', + !!formData.company ? 'text-brand' : 'text-scale-300', ].join(' ')} /> } @@ -135,7 +133,7 @@ const TicketCustomizationForm = ({ supabase, user }: Props) => { className={[ 'w-3 flex spin', IS_SAVING && 'text-scale-300', - !!formData.location ? 'text-brand-900' : 'text-scale-300', + !!formData.location ? 'text-brand' : 'text-scale-300', ].join(' ')} /> } diff --git a/apps/www/components/LaunchWeek/8/Ticket/form.module.css b/apps/www/components/LaunchWeek/8/Ticket/form.module.css index 5b6a078561b..d982d30f265 100644 --- a/apps/www/components/LaunchWeek/8/Ticket/form.module.css +++ b/apps/www/components/LaunchWeek/8/Ticket/form.module.css @@ -130,8 +130,8 @@ height: 56px; margin-top: var(--space-4x); border-radius: var(--space-2x); - border: 1px solid var(--brand); - background: var(--brand); + border: 1px solid hsl(var(--brand-default)); + background: hsl(var(--brand-default)); cursor: pointer; font-family: inherit; font-size: var(--text-md); @@ -170,12 +170,12 @@ .submit.default:hover, .submit.default:focus { background: black; - color: var(--brand); + color: hsl(var(--brand-default)); } .submit.default.generate-with-github:hover path, .submit.default.generate-with-github:focus path { - fill: var(--brand); + fill: hsl(var(--brand-default)); } .submit.error:hover, diff --git a/apps/www/components/Layouts/SectionContainer.tsx b/apps/www/components/Layouts/SectionContainer.tsx index db5ec705165..931294e211d 100644 --- a/apps/www/components/Layouts/SectionContainer.tsx +++ b/apps/www/components/Layouts/SectionContainer.tsx @@ -3,10 +3,12 @@ import classNames from 'classnames' interface Props { children: React.ReactNode className?: string + id?: string } -const SectionContainer = ({ children, className }: Props) => ( +const SectionContainer = ({ children, className, id }: Props) => (
    -
    +
    {product.name}
    diff --git a/apps/www/components/Nav/Company.tsx b/apps/www/components/Nav/Company.tsx index 7b263e0bc9b..620acfb2ef0 100644 --- a/apps/www/components/Nav/Company.tsx +++ b/apps/www/components/Nav/Company.tsx @@ -27,7 +27,7 @@ const Company = () => { stroke="currentColor" aria-hidden="true" > - +

    {text}

    @@ -82,7 +82,7 @@ const Company = () => {
    View all posts diff --git a/apps/www/components/Nav/Developers.tsx b/apps/www/components/Nav/Developers.tsx index 01cd98ebedf..71061cd2fc8 100644 --- a/apps/www/components/Nav/Developers.tsx +++ b/apps/www/components/Nav/Developers.tsx @@ -32,7 +32,7 @@ const Developers = () => { stroke="currentColor" aria-hidden="true" > - + )} {Svg && } diff --git a/apps/www/components/Nav/index.tsx b/apps/www/components/Nav/index.tsx index 2439d76df00..8f7e04225b0 100644 --- a/apps/www/components/Nav/index.tsx +++ b/apps/www/components/Nav/index.tsx @@ -85,7 +85,7 @@ const Nav = () => {

    {description}

    {url && ( -

    +

    )} @@ -120,7 +120,7 @@ const Nav = () => { >
    )} -
    +
    ) diff --git a/apps/www/components/SOC2RequestForm.tsx b/apps/www/components/SOC2RequestForm.tsx index fb45809b19f..841e7e0e5b9 100644 --- a/apps/www/components/SOC2RequestForm.tsx +++ b/apps/www/components/SOC2RequestForm.tsx @@ -68,7 +68,7 @@ const SOC2RequestForm = () => { return (
    {formSubmitted ? ( -

    +

    Thank you for your submission! A member from the Supabase team will reach out to you shortly!

    diff --git a/apps/www/components/ScrollProgress.tsx b/apps/www/components/ScrollProgress.tsx index ddbea562561..cd366d1c21f 100644 --- a/apps/www/components/ScrollProgress.tsx +++ b/apps/www/components/ScrollProgress.tsx @@ -30,7 +30,7 @@ const ScrollProgress = () => { return (
    {

    {card.abstract}

    {card.url && ( -
    Read Customer Story
    +
    Read Customer Story
    )}
    ) diff --git a/apps/www/components/Sections/CustomerQuotesSection.tsx b/apps/www/components/Sections/CustomerQuotesSection.tsx index bfcd363fc8a..4715d7f390a 100644 --- a/apps/www/components/Sections/CustomerQuotesSection.tsx +++ b/apps/www/components/Sections/CustomerQuotesSection.tsx @@ -74,7 +74,7 @@ const CustomerQuotesSection = (props: Props) => { {customer.author}, {customer.role}

    {customer.url && ( -
    +
    Read Customer Story
    diff --git a/apps/www/components/Sections/ProductHeaderCentered.tsx b/apps/www/components/Sections/ProductHeaderCentered.tsx index 9012a0f846d..98de8020142 100644 --- a/apps/www/components/Sections/ProductHeaderCentered.tsx +++ b/apps/www/components/Sections/ProductHeaderCentered.tsx @@ -53,7 +53,7 @@ const ProductHeaderCentered = (props: Types) => ( {props.icon && } {props.title && ( {props.title} diff --git a/apps/www/components/Sections/TimedTabsSection.tsx b/apps/www/components/Sections/TimedTabsSection.tsx index 532a70fb177..1de3cc0ddd7 100644 --- a/apps/www/components/Sections/TimedTabsSection.tsx +++ b/apps/www/components/Sections/TimedTabsSection.tsx @@ -29,7 +29,7 @@ const Tab = ({ isActive, label, paragraph, onClick, progress, intervalDuration } {isActive && ( 99.7 ? 'opacity-0' : 'opacity-100', ].join(' ')} style={{ x: `${progress! - 100}%` }} diff --git a/apps/www/data/partners/index.tsx b/apps/www/data/partners/index.tsx index 317e95ae89f..bb5399763c8 100644 --- a/apps/www/data/partners/index.tsx +++ b/apps/www/data/partners/index.tsx @@ -29,9 +29,9 @@ export default { aria-hidden="true" > diff --git a/apps/www/data/products/vector/pageData.tsx b/apps/www/data/products/vector/pageData.tsx index 2da32838233..2ae6b53155f 100644 --- a/apps/www/data/products/vector/pageData.tsx +++ b/apps/www/data/products/vector/pageData.tsx @@ -162,7 +162,7 @@ export default { <> Simple yet
    - powerful APIs + powerful APIs ), paragraph: 'Easy-to-use client libraries for managing and querying vector stores in Postgres.', @@ -242,7 +242,7 @@ docs.query( <> Powerful Features
    - Scale to millions + Scale to millions ), paragraph: diff --git a/apps/www/layouts/comparison.tsx b/apps/www/layouts/comparison.tsx index 020d31e2559..c832c800eda 100644 --- a/apps/www/layouts/comparison.tsx +++ b/apps/www/layouts/comparison.tsx @@ -90,7 +90,7 @@ const LayoutComparison = ({ components, props }: Props) => { {/* Title and description */}
    -

    Alternative

    +

    Alternative

    {props.blog.title}

    {props.blog.date}

    diff --git a/apps/www/lib/authors.json b/apps/www/lib/authors.json index d35168ab4c8..a612a143ea3 100644 --- a/apps/www/lib/authors.json +++ b/apps/www/lib/authors.json @@ -268,7 +268,7 @@ { "author_id": "gregnr", "author": "Greg Richardson", - "position": "Open Source Contributor", + "position": "Engineering", "author_url": "https://github.com/gregnr", "author_image_url": "https://github.com/gregnr.png" }, diff --git a/apps/www/lib/mdx/mdxComponents.tsx b/apps/www/lib/mdx/mdxComponents.tsx index 0ff6f996a7d..55ffb97b991 100644 --- a/apps/www/lib/mdx/mdxComponents.tsx +++ b/apps/www/lib/mdx/mdxComponents.tsx @@ -75,6 +75,11 @@ export default function mdxComponents(type?: 'blog' | 'lp' | undefined) { )} ), + Link: (props: HTMLAnchorElement) => ( +
    + {props.children} + + ), code: (props: any) => {props.children}, } diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index 9216ce46332..0ff808fff5f 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -2257,4 +2257,5 @@ module.exports = [ source: '/docs/guides/platform/oauth-apps/authorize-an-oauth-app', destination: '/docs/guides/platform/oauth-apps/build-a-supabase-integration', }, + { permanent: true, source: '/docs/reference/cli/config', destination: '/docs/guides/cli/config' }, ] diff --git a/apps/www/package.json b/apps/www/package.json index 55aacb7ebbb..d12b97bf9f4 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -60,7 +60,7 @@ "shared-data": "*", "swiper": "^6.5.0", "swr": "^0.5.4", - "three": "^0.154.0", + "three": "^0.136.0", "typescript": "^5.0.4", "ui": "*", "use-debounce": "^7.0.1", @@ -81,7 +81,7 @@ "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "^17.0.11", "@types/react-syntax-highlighter": "^15.5.6", - "@types/three": "^0.154.0", + "@types/three": "^0.136.0", "autoprefixer": "^10.4.14", "config": "*", "file-loader": "^6.2.0", diff --git a/apps/www/pages/beta.tsx b/apps/www/pages/beta.tsx index 8ac450a2b35..f72f5e5a8fd 100644 --- a/apps/www/pages/beta.tsx +++ b/apps/www/pages/beta.tsx @@ -118,7 +118,7 @@ const Hero = (props: Props) => {

    Supabase is an open source Firebase alternative.

    - Today, we're moving to Beta + Today, we're moving to Beta

  • Adopted the{' '} - + Snyk {' '} dependency monitor as part of our SSDLC on several key component of our system, to @@ -467,7 +462,7 @@ const Security = () => { now uses {' '} @@ -513,7 +508,7 @@ const Reliability = () => { https://status.supabase.com {' '} @@ -557,7 +552,7 @@ const NewFeaturesAndIntegrations = () => { JavaScript {' '} @@ -565,7 +560,7 @@ const NewFeaturesAndIntegrations = () => { HTTP @@ -587,7 +582,7 @@ const NewFeaturesAndIntegrations = () => { subscribe to changes in your database {' '} @@ -606,7 +601,7 @@ const NewFeaturesAndIntegrations = () => { querying from multiple tables {' '} @@ -614,7 +609,7 @@ const NewFeaturesAndIntegrations = () => { invoke complex functions @@ -703,7 +698,7 @@ const BetaPricing = () => ( rory@supabase.io @@ -716,7 +711,7 @@ const BetaPricing = () => (

    The Supabase Base plan is now called the Supabase Pro plan as per the{' '} - + pricing page . @@ -747,7 +742,7 @@ const OpenSource = () => ( sponsorship {' '} @@ -759,7 +754,7 @@ const OpenSource = () => ( partnering with Strive School {' '} @@ -772,7 +767,7 @@ const OpenSource = () => ( rory@supabase.io {' '} @@ -783,7 +778,7 @@ const OpenSource = () => ( our GitHub. {' '} @@ -819,7 +814,7 @@ const FundingPartners = () => ( Twitter {' '} @@ -856,7 +851,7 @@ const ScalingOurTeam = () => ( work@supabase.io @@ -917,7 +912,7 @@ const WhatsNext = () => ( support@supabase.io {' '} @@ -925,7 +920,7 @@ const WhatsNext = () => ( discussion {' '} diff --git a/apps/www/pages/blog/[slug].tsx b/apps/www/pages/blog/[slug].tsx index 1b1fcbd5b8b..356c3d5c107 100644 --- a/apps/www/pages/blog/[slug].tsx +++ b/apps/www/pages/blog/[slug].tsx @@ -202,7 +202,7 @@ function BlogPostPage(props: any) { {/* Title and description */}

    -

    Blog post

    +

    Blog post

    {props.blog.title}

    {props.blog.date}

    diff --git a/apps/www/pages/careers/index.tsx b/apps/www/pages/careers/index.tsx index a303e9f2e9a..f03c234b0a1 100644 --- a/apps/www/pages/careers/index.tsx +++ b/apps/www/pages/careers/index.tsx @@ -101,7 +101,7 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {
    -

    +

    Supabase Careers

    @@ -121,9 +121,9 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => { return (
    -
    +

    {company.number}

    @@ -153,7 +153,7 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => { Discord to stay connected to our team, and our community.

    -
    +

    We deeply believe in the efficacy of collaborative open source

    @@ -445,10 +445,10 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => { className="flex lg:block items-start space-x-6 lg:space-x-0 lg:w-full" >
    -

    +

    {i + 1}

    -
    +

    @@ -461,7 +461,7 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => {

    ) })} -

    +

    diff --git a/apps/www/pages/customers/[slug].tsx b/apps/www/pages/customers/[slug].tsx index 7be097ca5b9..19880b2ca34 100644 --- a/apps/www/pages/customers/[slug].tsx +++ b/apps/www/pages/customers/[slug].tsx @@ -122,9 +122,7 @@ function CaseStudyPage(props: any) {
    - - Customer Stories - + Customer Stories

    {props.blog.title} diff --git a/apps/www/pages/edge-functions/edge-functions.tsx b/apps/www/pages/edge-functions/edge-functions.tsx index 20900033d31..981685a0268 100644 --- a/apps/www/pages/edge-functions/edge-functions.tsx +++ b/apps/www/pages/edge-functions/edge-functions.tsx @@ -145,7 +145,7 @@ function Database() { className="bg-scale-100 dark:bg-scale-300 group flex flex-col gap-4 rounded border px-8 py-6" key={i} > -
    +
    {item.icon ? item.icon : }
    diff --git a/apps/www/pages/launch-week/6/index.tsx b/apps/www/pages/launch-week/6/index.tsx index 7f8f8426cc1..4912c976a6b 100644 --- a/apps/www/pages/launch-week/6/index.tsx +++ b/apps/www/pages/launch-week/6/index.tsx @@ -1018,7 +1018,7 @@ export default function launchweek() { href="https://supabase.com/blog/launch-week-6-hackathon" target="_blank" rel="nooper noreferrer" - className="text-brand-900" + className="text-brand" > blog post @@ -1052,7 +1052,7 @@ export default function launchweek() { Join our Discord @@ -1064,7 +1064,7 @@ export default function launchweek() {

    Submit your project through{' '} @@ -1091,7 +1091,7 @@ export default function launchweek() {

    Supabase Cache Helpers

    Previous Best Overall Project Winner

    @@ -1105,7 +1105,7 @@ export default function launchweek() {

    Super UI

    Previous Best Overall Project Runner Up

    @@ -1120,7 +1120,7 @@ export default function launchweek() {

    Quid Faciam Hodie?

    Winner Best Flutter Project

    @@ -1130,7 +1130,7 @@ export default function launchweek() {

    That Hot Dog Game

    Previous Winner: Most Fun/Interesting

    @@ -1145,7 +1145,7 @@ export default function launchweek() {

    RepoWatch

    Previous Winner Best Realtime Project

    @@ -1177,10 +1177,10 @@ export default function launchweek() { }} >
    -
    +
    @@ -1204,7 +1204,7 @@ export default function launchweek() { We worked with more than 30 content creators from around the world to drop a mountain of content simultaneously! -
    +
    See all the content
    @@ -1220,7 +1220,7 @@ export default function launchweek() { {activeCreator !== null && (

    {creators[activeCreator].description}

    )} -

    +

    {creators[activeCreator].link_title} diff --git a/apps/www/pages/launch-week/7/index.tsx b/apps/www/pages/launch-week/7/index.tsx index 55c3b4348aa..5043d62d84d 100644 --- a/apps/www/pages/launch-week/7/index.tsx +++ b/apps/www/pages/launch-week/7/index.tsx @@ -28,11 +28,11 @@ interface Props { const supabaseAdmin = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL ?? 'http://localhost:54321', + // ANON KEY process.env.SUPABASE_SERVICE_ROLE_SECRET ?? process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_SECRET ?? - '' + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I' ) - export default function TicketHome({ users }: Props) { const { isDarkMode, toggleTheme } = useTheme() diff --git a/apps/www/pages/launch-week/7/tickets/index.tsx b/apps/www/pages/launch-week/7/tickets/index.tsx index 61b1b0cd3a6..0027a45b7c6 100644 --- a/apps/www/pages/launch-week/7/tickets/index.tsx +++ b/apps/www/pages/launch-week/7/tickets/index.tsx @@ -131,11 +131,7 @@ export default function TicketsPage({ users }: Props) {

    Check out all the tickets

    -

    - Join us on April 16th for Launch Week 7's final day{' '} -
    and find out if you are one of the lucky - winners. Get sharing! -

    +

    Submissions are closed.

    diff --git a/apps/www/pages/launch-week/index.tsx b/apps/www/pages/launch-week/index.tsx index a44ac87bf85..d7405a095ad 100644 --- a/apps/www/pages/launch-week/index.tsx +++ b/apps/www/pages/launch-week/index.tsx @@ -1,25 +1,29 @@ -import { GetServerSideProps } from 'next' -import { NextSeo } from 'next-seo' import { useState, useEffect } from 'react' -import Image from 'next/image' +import { GetServerSideProps } from 'next' import dynamic from 'next/dynamic' +import Head from 'next/head' +import Image from 'next/image' import { useRouter } from 'next/router' +import { NextSeo } from 'next-seo' import { createClient, Session, SupabaseClient } from '@supabase/supabase-js' import { SITE_ORIGIN, SITE_URL } from '~/lib/constants' -import { PageState, ConfDataContext, UserData } from '~/components/LaunchWeek/hooks/use-conf-data' import DefaultLayout from '~/components/Layouts/Default' +import { PageState, ConfDataContext, UserData } from '~/components/LaunchWeek/hooks/use-conf-data' import SectionContainer from '~/components/Layouts/SectionContainer' import { LaunchWeekLogoHeader } from '~/components/LaunchWeek/8/LaunchWeekLogoHeader' +import { Meetup } from '~/components/LaunchWeek/8/LW8Meetups' +import LW8CalloutsSection from '~/components/LaunchWeek/8/LW8CalloutsSection' import { useTheme } from 'common/Providers' import 'swiper/swiper.min.css' -import Head from 'next/head' const AnimatedParticles = dynamic( () => import('~/components/LaunchWeek/8/AnimatedParticles/ParticlesCanvas') ) +const LW8Releases = dynamic(() => import('~/components/LaunchWeek/8/Releases')) +const LW8Meetups = dynamic(() => import('~/components/LaunchWeek/8/LW8Meetups')) const TicketContainer = dynamic(() => import('~/components/LaunchWeek/8/Ticket/TicketContainer')) const LaunchWeekPrizeSection = dynamic( () => import('~/components/LaunchWeek/8/LaunchWeekPrizeSection') @@ -29,6 +33,7 @@ const CTABanner = dynamic(() => import('~/components/CTABanner')) interface Props { users?: UserData[] + meetups?: Meetup[] } const supabaseAdmin = createClient( @@ -39,7 +44,7 @@ const supabaseAdmin = createClient( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk3MjcwMTIsImV4cCI6MTk4NTMwMzAxMn0.SZLqryz_-stF8dgzeVXmzZWPOqdOrBwqJROlFES8v3I' ) -export default function TicketHome({ users }: Props) { +export default function TicketHome({ users, meetups }: Props) { const { query } = useRouter() const TITLE = 'Supabase LaunchWeek 8' @@ -63,7 +68,7 @@ export default function TicketHome({ users }: Props) { } const [userData, setUserData] = useState(defaultUserData) - const [pageState, setPageState] = useState('ticket') + const [_, setPageState] = useState('ticket') useEffect(() => { if (!supabase) { @@ -138,31 +143,48 @@ export default function TicketHome({ users }: Props) {
    - {supabase && } + starts background
    -
    - -
    +
    +
    +
    -
    +
    + + + +
    + + + + + + + + + +
    {supabase && (
    )}
    - - + + {users && }
    @@ -185,15 +207,19 @@ export default function TicketHome({ users }: Props) { ) } -export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { +export const getServerSideProps: GetServerSideProps = async () => { // fetch users for the TicketBrickWall const { data: users } = await supabaseAdmin! .from('lw8_tickets_golden') - .select('username, golden', { count: 'exact' }) + .select('username, golden') + .limit(17) + + const { data: meetups } = await supabaseAdmin!.from('lw8_meetups').select('*') return { props: { users, + meetups, }, } } diff --git a/apps/www/pages/launch-week/tickets/[username].tsx b/apps/www/pages/launch-week/tickets/[username].tsx index 37808e679e4..0aa62979f24 100644 --- a/apps/www/pages/launch-week/tickets/[username].tsx +++ b/apps/www/pages/launch-week/tickets/[username].tsx @@ -114,7 +114,7 @@ export default function UsernamePage({ user, users, ogImageUrl }: Props) { )}
    -
    +
    -
    +
    {

    You can download our latest DPA document by submitting your email here.

    {formSubmitted ? ( -

    +

    Thank you for your submission! A new tab should have opened with the DPA document

    diff --git a/apps/www/pages/open-source/index.tsx b/apps/www/pages/open-source/index.tsx index 1aac63bdcbe..49d24b55151 100644 --- a/apps/www/pages/open-source/index.tsx +++ b/apps/www/pages/open-source/index.tsx @@ -82,8 +82,8 @@ const OpenSource = () => { {maintainerPills.map((x) => (
  • setActivePill(x)} > diff --git a/apps/www/pages/partners/experts/[slug].tsx b/apps/www/pages/partners/experts/[slug].tsx index ffb5f8fe409..af94fef4e3c 100644 --- a/apps/www/pages/partners/experts/[slug].tsx +++ b/apps/www/pages/partners/experts/[slug].tsx @@ -160,7 +160,7 @@ function Partner({
    Category - + {partner.category} @@ -172,7 +172,7 @@ function Partner({ href={partner.website} target="_blank" rel="noreferrer" - className="text-brand-900 hover:text-brand-800 transition-colors" + className="text-brand hover:text-brand-300 transition-colors" > {new URL(partner.website).host} @@ -185,7 +185,7 @@ function Partner({ href={partner.docs} target="_blank" rel="noreferrer" - className="text-brand-900 hover:text-brand-800 transition-colors" + className="text-brand hover:text-brand-300 transition-colors" > Learn diff --git a/apps/www/pages/partners/experts/index.tsx b/apps/www/pages/partners/experts/index.tsx index d1f152a074b..eef756d787e 100644 --- a/apps/www/pages/partners/experts/index.tsx +++ b/apps/www/pages/partners/experts/index.tsx @@ -78,11 +78,11 @@ function ExpertPartnersPage(props: Props) { fill="none" viewBox="0 0 24 24" stroke="currentColor" - stroke-width="1" + strokeWidth="1" > @@ -100,11 +100,11 @@ function ExpertPartnersPage(props: Props) { fill="none" viewBox="0 0 24 24" stroke="currentColor" - stroke-width="1" + strokeWidth="1" > diff --git a/apps/www/pages/partners/integrations/[slug].tsx b/apps/www/pages/partners/integrations/[slug].tsx index 95a87f58eba..76772b66469 100644 --- a/apps/www/pages/partners/integrations/[slug].tsx +++ b/apps/www/pages/partners/integrations/[slug].tsx @@ -201,7 +201,7 @@ function Partner({
    Category - + {partner.category} @@ -213,7 +213,7 @@ function Partner({ href={partner.website} target="_blank" rel="noreferrer" - className="text-brand-900 hover:text-brand-800 transition-colors" + className="text-brand hover:text-brand-300 transition-colors" > {new URL(partner.website).host} @@ -226,7 +226,7 @@ function Partner({ href={partner.docs} target="_blank" rel="noreferrer" - className="text-brand-900 hover:text-brand-800 transition-colors" + className="text-brand hover:text-brand-300 transition-colors" > Learn diff --git a/apps/www/pages/partners/integrations/index.tsx b/apps/www/pages/partners/integrations/index.tsx index 8eba3c43e9b..eccd6d0ba2c 100644 --- a/apps/www/pages/partners/integrations/index.tsx +++ b/apps/www/pages/partners/integrations/index.tsx @@ -202,10 +202,10 @@ function IntegrationPartnersPage(props: Props) { INTEGRATIONS - EXPERTS + EXPERTS - + BECOME A PARTNER diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx index 3940e1b06e6..1cc52e0a397 100644 --- a/apps/www/pages/pricing/index.tsx +++ b/apps/www/pages/pricing/index.tsx @@ -104,7 +104,7 @@ export default function IndexPage() { }) => { return (
    -

    +

    {plan}

    @@ -150,7 +150,7 @@ export default function IndexPage() {
    -

    Pricing

    +

    Pricing

    Predictable pricing, no surprises

    Start building for free, collaborate with a team, then scale to millions of users @@ -174,7 +174,7 @@ export default function IndexPage() { key={`row-${plan.name}`} className={[ plan.name === 'Pro' - ? 'bg-brand-1100 dark:bg-brand-900 border px-0.5 -mt-8 rounded-[6px]' + ? 'bg-brand-600 dark:bg-brand border px-0.5 -mt-8 rounded-[6px]' : '', ].join(' ')} > @@ -198,13 +198,13 @@ export default function IndexPage() {

    {plan.name}

    {plan.nameBadge && ( - + {plan.nameBadge} )} @@ -249,7 +249,7 @@ export default function IndexPage() { {plan.warning && (

    - + {plan.warning}

    @@ -273,7 +273,7 @@ export default function IndexPage() { {plan.features.map((feature) => (
  • ) } @@ -130,10 +134,15 @@ export interface IntegrationConnectionProps extends React.HTMLAttributes( - ({ className, connection, type, actions, ...props }, ref) => { + ( + { className, connection, type, actions, showNode = true, orientation = 'horizontal', ...props }, + ref + ) => { const { data: projects } = useProjectsQuery() const project = projects?.find((project) => project.ref === connection.supabase_project_ref) @@ -142,18 +151,31 @@ const IntegrationConnection = React.forwardRef -
    -
    -
    + {showNode && ( +
    + )} +
    +
    - {project?.name} + {project?.name} {!connection?.metadata?.framework ? (
    - +
    ) : ( {`icon`} )} - {connection.metadata?.name} + {connection.metadata?.name}
    - + Connected {dayjs(connection?.inserted_at).fromNow()} - + Added by {connection?.added_by?.primary_email}
    @@ -206,7 +228,7 @@ const IntegrationConnectionOption = React.forwardRef{connection.metadata.name}
    - + Connected {dayjs(connection.inserted_at).fromNow()}
    @@ -219,15 +241,23 @@ const IntegrationConnectionOption = React.forwardRef ->(({ className, ...props }, ref) => { + React.HTMLAttributes & { showNode?: boolean } +>(({ className, showNode = true, ...props }, ref) => { return (
    -
    + {showNode && ( +
    + )}
    void +} + +const OrganizationPicker = ({ + integrationName, + configurationId, + selectedOrg, + onSelectedOrgChange, +}: OrganizationPickerProps) => { + const [open, setOpen] = useState(false) + const ref = useRef(null) + + const { data: integrationData } = useIntegrationsQuery() + const { data: organizationsData, isLoading: isLoadingOrganization } = useOrganizationsQuery() + + const installed = useMemo( + () => + integrationData && organizationsData + ? getHasInstalledObject({ + integrationName, + integrationData, + organizationsData, + installationId: configurationId, + }) + : {}, + [configurationId, integrationData, integrationName, organizationsData] + ) + + return ( + <> + + + + + + + + + No results found. + + {organizationsData?.map((org) => { + return ( + { + const org = organizationsData?.find( + (org) => org.slug.toLowerCase() === slug.toLowerCase() + ) + if (org) { + onSelectedOrgChange(org) + } + + setOpen(false) + }} + > + + {org.name}{' '} + {configurationId && installed[org.slug] && ( + + Integration Installed + + )} + + ) + })} + + + + + + + ) +} + +export default OrganizationPicker diff --git a/studio/components/interfaces/Integrations/ProjectLinker.tsx b/studio/components/interfaces/Integrations/ProjectLinker.tsx index e220c54f1d2..2bebe5af4b7 100644 --- a/studio/components/interfaces/Integrations/ProjectLinker.tsx +++ b/studio/components/interfaces/Integrations/ProjectLinker.tsx @@ -1,13 +1,9 @@ -import { ENV_VAR_RAW_KEYS } from 'components/interfaces/Integrations/Integrations-Vercel.constants' -import { Markdown } from 'components/interfaces/Markdown' -import { vercelIcon } from 'components/to-be-cleaned/ListIcons' -import { useIntegrationConnectionsCreateMutation } from 'data/integrations/integration-connections-create-mutation' -import { useIntegrationsVercelConnectionSyncEnvsMutation } from 'data/integrations/integrations-vercel-connection-sync-envs-mutation' -import { VercelProjectsResponse } from 'data/integrations/integrations-vercel-projects-query' +import { ReactNode, useRef, useState } from 'react' + import { IntegrationProjectConnection } from 'data/integrations/integrations.types' +import { IntegrationConnectionsCreateVariables } from 'data/integrations/types' import { useSelectedOrganization } from 'hooks' import { BASE_PATH } from 'lib/constants' -import { useRef, useState } from 'react' import { toast } from 'react-hot-toast' import { Button, @@ -24,22 +20,33 @@ import { cn, } from 'ui' -interface Project { +export interface Project { id: string name: string ref: string } +export interface ForeignProject { + id: string + name: string +} + export interface ProjectLinkerProps { organizationIntegrationId: string | undefined - foreignProjects: VercelProjectsResponse[] + foreignProjects: ForeignProject[] supabaseProjects: Project[] - onCreateConnections?: () => void + onCreateConnections: (variables: IntegrationConnectionsCreateVariables) => void installedConnections: IntegrationProjectConnection[] | undefined - setLoading?: (x: boolean) => void - showSkip?: boolean + isLoading?: boolean + integrationIcon: ReactNode + getForeignProjectIcon?: (project: ForeignProject) => ReactNode + choosePrompt?: string + onSkip?: () => void loadingForeignProjects?: boolean loadingSupabaseProjects?: boolean + + defaultSupabaseProjectRef?: string + defaultForeignProjectId?: string } const ProjectLinker = ({ @@ -48,77 +55,63 @@ const ProjectLinker = ({ supabaseProjects, onCreateConnections: _onCreateConnections, installedConnections = [], - setLoading, - showSkip = false, + isLoading, + integrationIcon, + getForeignProjectIcon, + choosePrompt = 'Choose a project', + onSkip, loadingForeignProjects, loadingSupabaseProjects, + + defaultSupabaseProjectRef, + defaultForeignProjectId, }: ProjectLinkerProps) => { const [supabaseProjectsComboBoxOpen, setSupabaseProjectsComboboxOpen] = useState(false) - const [vercelProjectsComboBoxOpen, setVercelProjectsComboboxOpen] = useState(false) + const [foreignProjectsComboBoxOpen, setForeignProjectsComboboxOpen] = useState(false) const supabaseProjectsComboBoxRef = useRef(null) - const vercelProjectsComboBoxRef = useRef(null) + const foreignProjectsComboBoxRef = useRef(null) const selectedOrganization = useSelectedOrganization() - const [supabaseProjectRef, setSupabaseProjectRef] = useState(undefined) - const [vercelProjectId, setVercelProjectId] = useState(undefined) - - const { mutateAsync: syncEnvs } = useIntegrationsVercelConnectionSyncEnvsMutation() - const { mutate: createConnections, isLoading } = useIntegrationConnectionsCreateMutation({ - async onSuccess({ id }) { - try { - await syncEnvs({ connectionId: id }) - } catch (error: any) { - toast.error('Failed to sync environment variables: ', error.message) - } - - if (setLoading) setLoading(false) - _onCreateConnections?.() - }, - onError() { - if (setLoading) setLoading(false) - }, - }) + const [supabaseProjectRef, setSupabaseProjectRef] = useState( + defaultSupabaseProjectRef + ) + const [foreignProjectId, setForeignProjectId] = useState( + defaultForeignProjectId + ) // create a flat array of foreign project ids. ie, ["prj_MlkO6AiLG5ofS9ojKrkS3PhhlY3f", ..] const flatInstalledConnectionsIds = new Set(installedConnections.map((x) => x.foreign_project_id)) - // check that vercel project is not already installed - const filteredForeignProjects: VercelProjectsResponse[] = foreignProjects.filter( - (foreignProject) => { - return !flatInstalledConnectionsIds.has(foreignProject.id) - } - ) - const selectedSupabaseProject = supabaseProjectRef ? supabaseProjects.find((x) => x.ref?.toLowerCase() === supabaseProjectRef?.toLowerCase()) : undefined - const selectedVercelProject = vercelProjectId - ? filteredForeignProjects.find((x) => x.id?.toLowerCase() === vercelProjectId?.toLowerCase()) + const selectedForeignProject = foreignProjectId + ? foreignProjects.find((x) => x.id?.toLowerCase() === foreignProjectId?.toLowerCase()) : undefined function onCreateConnections() { - const projectDetails = selectedVercelProject + const projectDetails = selectedForeignProject if (!organizationIntegrationId) return console.error('No integration ID set') - if (!selectedVercelProject?.id) return console.error('No Vercel project ID set') + if (!selectedForeignProject?.id) return console.error('No Foreign project ID set') if (!selectedSupabaseProject?.ref) return console.error('No Supabase project ref set') - if (setLoading) setLoading(true) + const alreadyInstalled = flatInstalledConnectionsIds.has(foreignProjectId ?? '') + if (alreadyInstalled) { + return toast.error( + `Unable to connect to ${selectedForeignProject.name}: Selected repository already has an installed connection to a project` + ) + } - createConnections({ + _onCreateConnections({ organizationIntegrationId, connection: { - foreign_project_id: selectedVercelProject?.id, + foreign_project_id: selectedForeignProject?.id, supabase_project_ref: selectedSupabaseProject?.ref, metadata: { ...projectDetails, - supabaseConfig: { - projectEnvVars: { - write: true, - }, - }, }, }, orgSlug: selectedOrganization?.slug, @@ -162,7 +155,7 @@ const ProjectLinker = ({ type="default" size="medium" block - disabled={loadingSupabaseProjects} + disabled={defaultSupabaseProjectRef !== undefined || loadingSupabaseProjects} loading={loadingSupabaseProjects} className="justify-start" icon={ @@ -175,9 +168,11 @@ const ProjectLinker = ({
    } iconRight={ - - - + defaultSupabaseProjectRef === undefined ? ( + + + + ) : null } > {selectedSupabaseProject ? selectedSupabaseProject.name : 'Choose Project'} @@ -194,14 +189,14 @@ const ProjectLinker = ({ No results found. - {supabaseProjects.map((project) => { + {supabaseProjects.map((project, i) => { return ( { - if (ref) setSupabaseProjectRef(ref) + onSelect={() => { + if (project.ref) setSupabaseProjectRef(project.ref) setSupabaseProjectsComboboxOpen(false) }} > @@ -225,23 +220,16 @@ const ProjectLinker = ({
    - - - + {integrationIcon}
    No results found. - {filteredForeignProjects.map((project) => { + {foreignProjects.map((project, i) => { return ( { - if (id) setVercelProjectId(id) - setVercelProjectsComboboxOpen(false) + onSelect={() => { + if (project.id) setForeignProjectId(project.id) + setForeignProjectsComboboxOpen(false) }} > - {!project?.framework ? ( - vercelIcon - ) : ( - {`icon`} - )} + {getForeignProjectIcon?.(project) ?? integrationIcon} {project.name} ) @@ -319,12 +286,12 @@ const ProjectLinker = ({
    - {showSkip && ( + {onSkip !== undefined && (
    - { - return ` - \n - - \`${x}\` -` -})} -`} - />
    ) } diff --git a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index c463ce98397..7a864aef626 100644 --- a/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -132,7 +132,7 @@ const AuditLogs = () => { return ( Your organization has a log retention period of{' '} - + {retentionPeriod} day {retentionPeriod > 1 ? 's' : ''} diff --git a/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx b/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx index f0012187c9a..d7f869fb452 100644 --- a/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx @@ -1,13 +1,12 @@ -import { FC } from 'react' import { Badge } from 'ui' -interface Props { +interface CreditBalanceProps { balance: string | number isCredit: boolean isDebt: boolean } -const CreditBalance: FC = ({ balance, isCredit, isDebt }) => { +const CreditBalance = ({ balance, isCredit, isDebt }: CreditBalanceProps) => { return (
    diff --git a/studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx b/studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx index dd7c89c898f..87ea901dfea 100644 --- a/studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx @@ -1,17 +1,17 @@ -import Link from 'next/link' import dayjs from 'dayjs' -import { FC, useEffect } from 'react' +import Link from 'next/link' +import { useEffect } from 'react' import { IconChevronRight, IconLoader } from 'ui' -import { useStore } from 'hooks' import Panel from 'components/ui/Panel' import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query' +import { useStore } from 'hooks' interface ProjectSummaryProps { project: any } -const ProjectSummary: FC = ({ project }) => { +const ProjectSummary = ({ project }: ProjectSummaryProps) => { const { ui } = useStore() const { data: subscription, @@ -68,7 +68,7 @@ interface ProjectsSummaryProps { projects: any } -const ProjectsSummary: FC = ({ projects }) => { +const ProjectsSummary = ({ projects }: ProjectsSummaryProps) => { return (

    Projects at a glance

    diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/BillingSettings.constants.ts b/studio/components/interfaces/Organization/BillingSettingsV2/BillingSettings.constants.ts index 3e16e7b2138..d2f5238e5f4 100644 --- a/studio/components/interfaces/Organization/BillingSettingsV2/BillingSettings.constants.ts +++ b/studio/components/interfaces/Organization/BillingSettingsV2/BillingSettings.constants.ts @@ -8,7 +8,7 @@ export const CANCELLATION_REASONS = [ 'Dashboard is too complicated', 'Postgres is too complicated', 'Problem not solved', - 'Bug issues', + 'Too many bugs/issues', 'I decided to use something else', 'My work has finished/discontinued', 'I’m migrating to/starting a new project', diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/EnterpriseCard.tsx b/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/EnterpriseCard.tsx index af9608f0dd0..cbf139f924b 100644 --- a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/EnterpriseCard.tsx +++ b/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/EnterpriseCard.tsx @@ -18,13 +18,13 @@ const EnterpriseCard = ({ plan, isCurrentPlan }: EnterpriseCardProps) => { >
    -

    {plan.name}

    +

    {plan.name}

    {isCurrentPlan ? (
    Current plan
    ) : plan.nameBadge ? ( -
    +
    {plan.nameBadge}
    ) : null} @@ -43,7 +43,7 @@ const EnterpriseCard = ({ plan, isCurrentPlan }: EnterpriseCardProps) => {
      {plan.features.map((feature) => (
    • -
    • ))} diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PlanUpdateSidePanel.tsx b/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PlanUpdateSidePanel.tsx index fd50323a60e..9ec255913b8 100644 --- a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PlanUpdateSidePanel.tsx +++ b/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PlanUpdateSidePanel.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrgPlansQuery } from 'data/subscriptions/org-plans-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation' +import { SubscriptionTier, useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation' import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks' import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants' import Telemetry from 'lib/telemetry' @@ -110,9 +110,14 @@ const PlanUpdateSidePanel = () => { return ui.setNotification({ category: 'error', message: 'Please select a payment method' }) } - updateOrgSubscription({ slug, tier: selectedTier, paymentMethod: selectedPaymentMethod }) + // If the user is downgrading from team, should have spend cap disabled by default + const tier = subscription?.plan?.id === 'team' && selectedTier === PRICING_TIER_PRODUCT_IDS.PRO ? PRICING_TIER_PRODUCT_IDS.PAYG as SubscriptionTier : selectedTier + + updateOrgSubscription({ slug, tier, paymentMethod: selectedPaymentMethod }) } + const planMeta = selectedTier ? availablePlans.find((p) => p.id === selectedTier.split('tier_')[1]) : null + return ( <> { >
      -

      {plan.name}

      +

      {plan.name}

      {isCurrentPlan ? (
      Current plan
      ) : plan.nameBadge ? ( -
      +
      {plan.nameBadge}
      ) : ( @@ -182,7 +187,7 @@ const PlanUpdateSidePanel = () => {

      {tierMeta?.costUnitOrg}

      -
      +
      {tierMeta?.warning}
      @@ -250,7 +255,7 @@ const PlanUpdateSidePanel = () => {