diff --git a/.cursor/rules/unit-integration-testing.mdc b/.cursor/rules/unit-integration-testing.mdc new file mode 100644 index 00000000000..e3a659a7c53 --- /dev/null +++ b/.cursor/rules/unit-integration-testing.mdc @@ -0,0 +1,6 @@ +--- +description: +globs: apps/studio/**/*.test.ts,apps/studio/**/*.test.tsx +alwaysApply: false +--- +Make sure to follow the guidelines in this file to write tests: [README.md](mdc:apps/studio/tests/README.md) diff --git a/.github/workflows/docs-lint-v2-scheduled.yml b/.github/workflows/docs-lint-v2-scheduled.yml index bdb8209632e..a6cfd2506a3 100644 --- a/.github/workflows/docs-lint-v2-scheduled.yml +++ b/.github/workflows/docs-lint-v2-scheduled.yml @@ -31,10 +31,10 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ - key: e5609278486869b545fda23b5732727b6e70a1c1 + key: b848086c298be920a40aa9b26c65c7575ae8deca - name: install linter if: steps.cache-cargo.outputs.cache-hit != 'true' - run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev e5609278486869b545fda23b5732727b6e70a1c1 + run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev b848086c298be920a40aa9b26c65c7575ae8deca - name: run linter env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docs-lint-v2.yml b/.github/workflows/docs-lint-v2.yml index aa977fcd5c4..95735c2d117 100644 --- a/.github/workflows/docs-lint-v2.yml +++ b/.github/workflows/docs-lint-v2.yml @@ -38,10 +38,10 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ - key: e5609278486869b545fda23b5732727b6e70a1c1 + key: b848086c298be920a40aa9b26c65c7575ae8deca - name: install linter if: steps.filter.outputs.docs == 'true' && steps.cache-cargo.outputs.cache-hit != 'true' - run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev e5609278486869b545fda23b5732727b6e70a1c1 + run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev b848086c298be920a40aa9b26c65c7575ae8deca - name: install reviewdog if: steps.filter.outputs.docs == 'true' uses: reviewdog/action-setup@3f401fe1d58fe77e10d665ab713057375e39b887 # v1.3.0 diff --git a/.github/workflows/studio-unit-tests.yml b/.github/workflows/studio-unit-tests.yml index 317ea8dab14..de776aa2630 100644 --- a/.github/workflows/studio-unit-tests.yml +++ b/.github/workflows/studio-unit-tests.yml @@ -24,9 +24,12 @@ permissions: contents: read jobs: - check: + test: # Uses larger hosted runner as it significantly decreases build times runs-on: [larger-runner-4cpu] + strategy: + matrix: + test_number: [1] steps: - uses: actions/checkout@v4 @@ -50,5 +53,25 @@ jobs: env: # Default is 2 GB, increase to have less frequent OOM errors NODE_OPTIONS: '--max_old_space_size=3072' - run: pnpm run test:studio - working-directory: ./ + run: pnpm run test:ci + working-directory: ./apps/studio + + - name: Upload coverage results to Coveralls + uses: coverallsapp/github-action@master + with: + parallel: true + flag-name: studio-tests + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: ./apps/studio/coverage/lcov.info + base-path: './apps/studio' + + finish: + needs: test + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@master + with: + parallel-finished: true + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index c579d052023..6284f0cae2b 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -15,8 +15,11 @@ permissions: contents: read jobs: - build: + test: runs-on: ubuntu-latest + strategy: + matrix: + test_number: [1] steps: - uses: actions/checkout@v4 @@ -39,4 +42,25 @@ jobs: run: pnpm i - name: Run tests - run: pnpm run test:ui + run: pnpm run test:ci + working-directory: ./packages/ui + + - name: Upload coverage results to Coveralls + uses: coverallsapp/github-action@master + with: + parallel: true + flag-name: ui-tests + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: ./packages/ui/coverage/lcov.info + base-path: './packages/ui' + + finish: + needs: test + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@master + with: + parallel-finished: true + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index b6ce235b350..966c0688383 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,11 @@ gcloud.json # sitemaps # apps/www/public/*.xml # apps/docs/public/*.xml + +# CLI version file +.temp/cli-latest + +.pnpm-store/* + +# Sentry CLI config +**/.sentryclirc diff --git a/apps/design-system/content/docs/components/button.mdx b/apps/design-system/content/docs/components/button.mdx index 8b7cda6325a..53c183989c5 100644 --- a/apps/design-system/content/docs/components/button.mdx +++ b/apps/design-system/content/docs/components/button.mdx @@ -81,7 +81,7 @@ Used for actions that might have a side effect, but not as serious as a destruct #### Destructive (currently `danger`) -Used for actions that will have a serios destructive side effect, like deleting data. +Used for actions that will have a serious destructive side effect, like deleting data. prop `type` will probably be changed to `destructive` in the future. diff --git a/apps/design-system/content/docs/fragments/inner-side-menu.mdx b/apps/design-system/content/docs/fragments/inner-side-menu.mdx index 329252c1efe..41f82d9dee3 100644 --- a/apps/design-system/content/docs/fragments/inner-side-menu.mdx +++ b/apps/design-system/content/docs/fragments/inner-side-menu.mdx @@ -44,7 +44,7 @@ function app() { If you don't need the Collapsible features, you can instead use ``. This doesn't require the use of the `` parent component wrapped around it. -You can wrap the `` conponents however you like. Below we've put a `
` around them. +You can wrap the `` components however you like. Below we've put a `
` around them. ```jsx {1} Projects @@ -125,7 +125,7 @@ Use the `actions` prop to add any actions like a ` - +const ErrorPage = ({ error }) => { + useEffect(() => { + Sentry.captureException(error) + }, [error]) + + return ( +
+ + Sorry, something went wrong + +
+ + +
-
-) + ) +} export default ErrorPage diff --git a/apps/docs/app/global-error.tsx b/apps/docs/app/global-error.tsx new file mode 100644 index 00000000000..52071703471 --- /dev/null +++ b/apps/docs/app/global-error.tsx @@ -0,0 +1,23 @@ +'use client' + +import * as Sentry from '@sentry/nextjs' +import NextError from 'next/error' +import { useEffect } from 'react' + +export default function GlobalError({ error }: { error: Error & { digest?: string } }) { + useEffect(() => { + Sentry.captureException(error) + }, [error]) + + return ( + + + {/* `NextError` is the default Next.js error page component. Its type + definition requires a `statusCode` prop. However, since the App Router + does not expose status codes for errors, we simply pass 0 to render a + generic error message. */} + + + + ) +} diff --git a/apps/docs/app/graphiql/page.tsx b/apps/docs/app/graphiql/page.tsx new file mode 100644 index 00000000000..671b5525963 --- /dev/null +++ b/apps/docs/app/graphiql/page.tsx @@ -0,0 +1,33 @@ +'use client' + +import dynamic from 'next/dynamic' +import { notFound } from 'next/navigation' +import { type ComponentProps } from 'react' +import { IS_DEV } from '~/lib/constants' + +const LazyGraphiQL = dynamic( + async () => { + const { GraphiQL: GraphiQLPrimitive } = await import('graphiql') + const { createGraphiQLFetcher } = await import('@graphiql/toolkit') + await import('graphiql/style.css') + + return function GraphiQL(props: Omit, 'fetcher'>) { + const fetcher = createGraphiQLFetcher({ + url: 'http://localhost:3001/docs/api/graphql', + }) + return + } + }, + { + loading: () =>
Loading GraphiQL...
, + ssr: false, + } +) + +export default function GraphiQLPage() { + if (!IS_DEV) { + notFound() + } + + return +} diff --git a/apps/docs/app/guides/ai/[[...slug]]/page.tsx b/apps/docs/app/guides/ai/[[...slug]]/page.tsx index 50198e532f3..a28fcec584a 100644 --- a/apps/docs/app/guides/ai/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/ai/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const AiGuidePage = async ({ params }: { params: Params }) => { +const AiGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['ai', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/ai/python/[slug]/page.tsx b/apps/docs/app/guides/ai/python/[slug]/page.tsx index 8ebfa5dde15..0edb4efe589 100644 --- a/apps/docs/app/guides/ai/python/[slug]/page.tsx +++ b/apps/docs/app/guides/ai/python/[slug]/page.tsx @@ -55,7 +55,8 @@ interface Params { slug: string } -const PythonClientDocs = async ({ params }: { params: Params }) => { +const PythonClientDocs = async (props: { params: Promise }) => { + const params = await props.params const { meta, ...data } = await getContent(params) const options = { diff --git a/apps/docs/app/guides/api/[[...slug]]/page.tsx b/apps/docs/app/guides/api/[[...slug]]/page.tsx index 85ec5923214..a4a486a7d12 100644 --- a/apps/docs/app/guides/api/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/api/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const ApiGuidePage = async ({ params }: { params: Params }) => { +const ApiGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['api', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/auth/[[...slug]]/page.tsx b/apps/docs/app/guides/auth/[[...slug]]/page.tsx index ddadcad5a12..c114d44da86 100644 --- a/apps/docs/app/guides/auth/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/auth/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const AuthGuidePage = async ({ params }: { params: Params }) => { +const AuthGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['auth', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/cron/[[...slug]]/page.tsx b/apps/docs/app/guides/cron/[[...slug]]/page.tsx index fb55f0fac85..f8a57df5367 100644 --- a/apps/docs/app/guides/cron/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/cron/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const CronGuidePage = async ({ params }: { params: Params }) => { +const CronGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['cron', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/database/[[...slug]]/page.tsx b/apps/docs/app/guides/database/[[...slug]]/page.tsx index 0f92d872d92..fde70f6e2b3 100644 --- a/apps/docs/app/guides/database/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const DatabaseGuidePage = async ({ params }: { params: Params }) => { +const DatabaseGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['database', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx index 0bbbee54595..7589b023bb5 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -4,29 +4,97 @@ import { readFile } from 'node:fs/promises' import { join, relative } from 'node:path' import rehypeSlug from 'rehype-slug' import emoji from 'remark-emoji' - +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, genGuidesStaticParams, removeRedundantH1, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { REVALIDATION_TAGS } from '~/features/helpers.fetch' import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs' 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' -import { REVALIDATION_TAGS } from '~/features/helpers.fetch' +import { octokit } from '~/lib/octokit' export const dynamicParams = false // We fetch these docs at build time from an external repo const org = 'supabase' const repo = 'wrappers' -const branch = 'main' const docsDir = 'docs/catalog' const externalSite = 'https://supabase.github.io/wrappers' +type TagQueryResponse = { + repository: { + refs: { + nodes: + | { + name: string + }[] + | null + pageInfo: { + hasNextPage: boolean + endCursor: string | null + } + } + } +} + +const tagQuery = ` + query TagQuery($owner: String!, $name: String!, $after: String) { + repository(owner: $owner, name: $name) { + refs( + refPrefix: "refs/tags/", + orderBy: { + field: TAG_COMMIT_DATE, + direction: DESC + }, + first: 5, + after: $after + ) { + nodes { + name + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + ` + +async function getLatestRelease(after: string | null = null) { + try { + const { + repository: { + refs: { + nodes, + pageInfo: { hasNextPage, endCursor }, + }, + }, + } = await octokit().graphql(tagQuery, { + owner: org, + name: repo, + after, + request: { + fetch: (url: RequestInfo | URL, options?: RequestInit) => + fetch(url, { ...options, next: { tags: [REVALIDATION_TAGS.WRAPPERS] } }), + }, + }) + + return ( + nodes?.find((node) => node?.name?.match(/^docs_v\d+\.\d+\.\d+/))?.name ?? + (hasNextPage && endCursor ? await getLatestRelease(endCursor) : null) + ) + } catch (error) { + console.error(`Error fetching release tags for wrappers federated pages: ${error}`) + return null + } +} + // Each external docs page is mapped to a local page const pageMap = [ { @@ -140,7 +208,8 @@ interface Params { slug?: string[] } -const WrappersDocs = async ({ params }: { params: Params }) => { +const WrappersDocs = async (props: { params: Promise }) => { + const params = await props.params const { isExternal, meta, ...data } = await getContent(params) const options = isExternal @@ -193,10 +262,17 @@ const getContent = async (params: Params) => { isExternal = true let remoteFile: string ;({ remoteFile, meta } = federatedPage) - const repoPath = `${org}/${repo}/${branch}/${docsDir}/${remoteFile}` - editLink = `${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}` + + const tag = await getLatestRelease() + if (!tag) { + throw new Error('No latest release found for federated wrappers pages') + } + + const repoPath = `${org}/${repo}/${tag}/${docsDir}/${remoteFile}` + editLink = `${org}/${repo}/blob/${tag}/${docsDir}/${remoteFile}` const response = await fetch(`https://raw.githubusercontent.com/${repoPath}`, { + cache: 'force-cache', next: { tags: [REVALIDATION_TAGS.WRAPPERS] }, }) const rawContent = await response.text() @@ -259,4 +335,4 @@ const generateStaticParams = async () => { const generateMetadata = genGuideMeta(getContent) export default WrappersDocs -export { generateStaticParams, generateMetadata } +export { generateMetadata, generateStaticParams } diff --git a/apps/docs/app/guides/deployment/[[...slug]]/page.tsx b/apps/docs/app/guides/deployment/[[...slug]]/page.tsx index 5c4311b6ede..b9c78bf2b21 100644 --- a/apps/docs/app/guides/deployment/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/deployment/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const DeploymentGuidePage = async ({ params }: { params: Params }) => { +const DeploymentGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['deployment', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/deployment/ci/[slug]/page.tsx b/apps/docs/app/guides/deployment/ci/[slug]/page.tsx index c505be8f3bf..756ac13b384 100644 --- a/apps/docs/app/guides/deployment/ci/[slug]/page.tsx +++ b/apps/docs/app/guides/deployment/ci/[slug]/page.tsx @@ -54,7 +54,8 @@ const pageMap = [ type Params = { slug: string } -const ActionDocs = async ({ params }: { params: Params }) => { +const ActionDocs = async (props: { params: Promise }) => { + const params = await props.params const { meta, ...data } = await getContent(params) const options = { diff --git a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx index bd8e51bde05..72b49c49a85 100644 --- a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx @@ -42,7 +42,8 @@ interface Params { slug?: string[] } -const TerraformDocs = async ({ params }: { params: Params }) => { +const TerraformDocs = async (props: { params: Promise }) => { + const params = await props.params const { meta, ...data } = await getContent(params) const options = { diff --git a/apps/docs/app/guides/functions/[[...slug]]/page.tsx b/apps/docs/app/guides/functions/[[...slug]]/page.tsx index e6f2c65a84b..9a2d3afc802 100644 --- a/apps/docs/app/guides/functions/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/functions/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const FunctionsGuidePage = async ({ params }: { params: Params }) => { +const FunctionsGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['functions', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/getting-started/[[...slug]]/page.tsx b/apps/docs/app/guides/getting-started/[[...slug]]/page.tsx index 429055c817a..130dbe1455a 100644 --- a/apps/docs/app/guides/getting-started/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/getting-started/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const GettingStartedGuidePage = async ({ params }: { params: Params }) => { +const GettingStartedGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['getting-started', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts index 0c8f83d7f0c..31b068fcf08 100644 --- a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts +++ b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts @@ -79,7 +79,8 @@ async function getAiPromptImpl(prompt: string) { } export const getAiPrompt = cache(getAiPromptImpl) -export async function generateAiPromptMetadata({ params: { slug } }: { params: { slug: string } }) { +export async function generateAiPromptMetadata(props: { params: Promise<{ slug: string }> }) { + const { slug } = await props.params const prompt = await getAiPrompt(slug) return { diff --git a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx index d098909dfd9..864f46d17b7 100644 --- a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx +++ b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx @@ -7,7 +7,11 @@ import { export const dynamicParams = false -export default async function AiPromptsPage({ params: { slug } }: { params: { slug: string } }) { +export default async function AiPromptsPage(props: { params: Promise<{ slug: string }> }) { + const params = await props.params + + const { slug } = params + let { heading, content } = await getAiPrompt(slug) content = ` ## How to use diff --git a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx index 69ba3cf793b..9572b7fc75e 100644 --- a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx @@ -108,7 +108,8 @@ interface Params { slug?: string[] } -const PGGraphQLDocs = async ({ params }: { params: Params }) => { +const PGGraphQLDocs = async (props: { params: Promise }) => { + const params = await props.params const { meta, ...data } = await getContent(params) const options = { @@ -137,7 +138,7 @@ const getContent = async ({ slug }: Params) => { const response = await fetch( `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`, - { next: { tags: [REVALIDATION_TAGS.GRAPHQL] } } + { cache: 'force-cache', next: { tags: [REVALIDATION_TAGS.GRAPHQL] } } ) const content = await response.text() diff --git a/apps/docs/app/guides/integrations/[[...slug]]/page.tsx b/apps/docs/app/guides/integrations/[[...slug]]/page.tsx index 34f193e076b..a7aa94254f4 100644 --- a/apps/docs/app/guides/integrations/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/integrations/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const IntegrationsGuidePage = async ({ params }: { params: Params }) => { +const IntegrationsGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['integrations', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/integrations/layout.tsx b/apps/docs/app/guides/integrations/layout.tsx index 7b5943f2253..c319779007f 100644 --- a/apps/docs/app/guides/integrations/layout.tsx +++ b/apps/docs/app/guides/integrations/layout.tsx @@ -1,7 +1,7 @@ import { IS_PLATFORM } from 'common' import { unstable_cache } from 'next/cache' - import { type NavMenuSection } from '~/components/Navigation/Navigation.types' +import { REVALIDATION_TAGS } from '~/features/helpers.fetch' import Layout from '~/layouts/guides' import { supabaseMisc } from '~/lib/supabaseMisc' @@ -13,7 +13,9 @@ export default async function IntegrationsLayout({ children }: { children: React // Will need to turn on revalidation later, just turning it off for now so we // can slowly turn things back on while monitoring usage -const getPartners = unstable_cache(getPartnersImpl, [], { revalidate: false }) +const getPartners = unstable_cache(getPartnersImpl, [], { + tags: [REVALIDATION_TAGS.PARTNERS], +}) async function getPartnersImpl() { if (!IS_PLATFORM) return [] diff --git a/apps/docs/app/guides/local-development/[[...slug]]/page.tsx b/apps/docs/app/guides/local-development/[[...slug]]/page.tsx index 1bd6814e8c1..006d4f49ac3 100644 --- a/apps/docs/app/guides/local-development/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/local-development/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const LocalDevelopmentGuidePage = async ({ params }: { params: Params }) => { +const LocalDevelopmentGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['local-development', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/platform/[[...slug]]/page.tsx b/apps/docs/app/guides/platform/[[...slug]]/page.tsx index 0c76803590a..3f0a8c13b28 100644 --- a/apps/docs/app/guides/platform/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/platform/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const PlatformGuidePage = async ({ params }: { params: Params }) => { +const PlatformGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['platform', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/queues/[[...slug]]/page.tsx b/apps/docs/app/guides/queues/[[...slug]]/page.tsx index 364dbb83475..42e5de3b554 100644 --- a/apps/docs/app/guides/queues/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/queues/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const QueuesGuidePage = async ({ params }: { params: Params }) => { +const QueuesGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['queues', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/realtime/[[...slug]]/page.tsx b/apps/docs/app/guides/realtime/[[...slug]]/page.tsx index 79e91ca9222..c65eb2ae994 100644 --- a/apps/docs/app/guides/realtime/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/realtime/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ import { getEmptyArray } from '~/features/helpers.fn' type Params = { slug?: string[] } -const RealtimeGuidePage = async ({ params }: { params: Params }) => { +const RealtimeGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['realtime', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/resources/[[...slug]]/page.tsx b/apps/docs/app/guides/resources/[[...slug]]/page.tsx index ca23754cc5a..f3f98d791cb 100644 --- a/apps/docs/app/guides/resources/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/resources/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const ResourcesGuidePage = async ({ params }: { params: Params }) => { +const ResourcesGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['resources', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/security/[[...slug]]/page.tsx b/apps/docs/app/guides/security/[[...slug]]/page.tsx index 8b7d4805aa5..0e8fb207219 100644 --- a/apps/docs/app/guides/security/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/security/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const SecurityGuidePage = async ({ params }: { params: Params }) => { +const SecurityGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['security', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/self-hosting/[[...slug]]/page.tsx b/apps/docs/app/guides/self-hosting/[[...slug]]/page.tsx index df826de3827..11b33e3894d 100644 --- a/apps/docs/app/guides/self-hosting/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/self-hosting/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const SelfHostingGuidePage = async ({ params }: { params: Params }) => { +const SelfHostingGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['self-hosting', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/storage/[[...slug]]/page.tsx b/apps/docs/app/guides/storage/[[...slug]]/page.tsx index 0f379466f54..67ad5d42301 100644 --- a/apps/docs/app/guides/storage/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/storage/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ import { getEmptyArray } from '~/features/helpers.fn' type Params = { slug?: string[] } -const StorageGuidePage = async ({ params }: { params: Params }) => { +const StorageGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['storage', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/telemetry/[[...slug]]/page.tsx b/apps/docs/app/guides/telemetry/[[...slug]]/page.tsx index 3ad5e6c392e..69dfb1381d2 100644 --- a/apps/docs/app/guides/telemetry/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/telemetry/[[...slug]]/page.tsx @@ -9,7 +9,8 @@ export const dynamicParams = false type Params = { slug?: string[] } -const MonitoringTroubleshootingGuidePage = async ({ params }: { params: Params }) => { +const MonitoringTroubleshootingGuidePage = async (props: { params: Promise }) => { + const params = await props.params const slug = ['telemetry', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) diff --git a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx index 9fcd77530d1..d56363997a1 100644 --- a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx +++ b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx @@ -8,11 +8,13 @@ import { PROD_URL } from '~/lib/constants' // export const revalidate = 86_400 export const dynamicParams = false -export default async function TroubleshootingEntryPage({ - params: { slug }, -}: { - params: { slug: string } +export default async function TroubleshootingEntryPage(props: { + params: Promise<{ slug: string }> }) { + const params = await props.params + + const { slug } = params + const allTroubleshootingEntries = await getAllTroubleshootingEntries() const entry = allTroubleshootingEntries.find((entry) => getArticleSlug(entry) === slug) @@ -23,7 +25,11 @@ export default async function TroubleshootingEntryPage({ return } -export const generateMetadata = async ({ params: { slug } }: { params: { slug: string } }) => { +export const generateMetadata = async (props: { params: Promise<{ slug: string }> }) => { + const params = await props.params + + const { slug } = params + const allTroubleshootingEntries = await getAllTroubleshootingEntries() const entry = allTroubleshootingEntries.find((entry) => getArticleSlug(entry) === slug) diff --git a/apps/docs/app/reference/[...slug]/page.tsx b/apps/docs/app/reference/[...slug]/page.tsx index 617ed1fc6f4..2c7bb70463b 100644 --- a/apps/docs/app/reference/[...slug]/page.tsx +++ b/apps/docs/app/reference/[...slug]/page.tsx @@ -14,11 +14,11 @@ import { export const dynamicParams = false -export default async function ReferencePage({ - params: { slug }, -}: { - params: { slug: Array } -}) { +export default async function ReferencePage(props: { params: Promise<{ slug: Array }> }) { + const params = await props.params + + const { slug } = params + if (!Object.keys(REFERENCES).includes(slug[0].replaceAll('-', '_'))) { notFound() } diff --git a/apps/docs/codegen.ts b/apps/docs/codegen.ts new file mode 100644 index 00000000000..6b52a4cc4c5 --- /dev/null +++ b/apps/docs/codegen.ts @@ -0,0 +1,13 @@ +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + overwrite: true, + schema: './__generated__/schema.graphql', + generates: { + '__generated__/graphql.ts': { + plugins: ['typescript', 'typescript-resolvers'], + }, + }, +} + +export default config diff --git a/apps/docs/components/Feedback/Feedback.tsx b/apps/docs/components/Feedback/Feedback.tsx index 16771ca6e5b..e2aa841aa43 100644 --- a/apps/docs/components/Feedback/Feedback.tsx +++ b/apps/docs/components/Feedback/Feedback.tsx @@ -18,7 +18,7 @@ import { Button, cn } from 'ui' import { IS_PLATFORM } from '~/lib/constants' import { useSendFeedbackMutation } from '~/lib/fetch/feedback' import { useSendTelemetryEvent } from '~/lib/telemetry' -import { getNotionTeam, getSanitizedTabParams } from './Feedback.utils' +import { getLinearTeam, getSanitizedTabParams } from './Feedback.utils' import { type FeedbackFields, FeedbackModal } from './FeedbackModal' const FeedbackButton = forwardRef< @@ -134,7 +134,7 @@ function Feedback({ className }: { className?: string }) { title, // @ts-expect-error -- can't click this button without having a state.response isHelpful: state.response === 'yes', - team: getNotionTeam(pathname), + team: getLinearTeam(pathname), }) setModalOpen(false) refocusButton() diff --git a/apps/docs/components/Feedback/Feedback.utils.ts b/apps/docs/components/Feedback/Feedback.utils.ts index cf9ff4c989a..3337d2f57ee 100644 --- a/apps/docs/components/Feedback/Feedback.utils.ts +++ b/apps/docs/components/Feedback/Feedback.utils.ts @@ -3,8 +3,8 @@ import { pick } from 'lodash' /** * Gets the Notion team to send feedback to based on the pathname. */ -const getNotionTeam = (pathname: string) => { - const DEFAULT_TEAM = 'team-docs' +const getLinearTeam = (pathname: string) => { + const DEFAULT_TEAM = 'Docs' // Pathname has format `/guides/(team)/**` const pathParts = pathname.split('/') @@ -13,26 +13,28 @@ const getNotionTeam = (pathname: string) => { switch (pathParts[2]) { case 'database': - return 'team-postgres' + return 'Postgres' case 'auth': - return 'team-auth' + return 'Auth' case 'storage': - return 'team-storage' + return 'Storage' case 'functions': - return 'team-functions' + return 'Functions' case 'realtime': - return 'team-realtime' + return 'Realtime' case 'ai': - return 'team-ai' + return 'AI' case 'local-development': case 'self-hosting': case 'deployment': - return 'team-dev-workflows' + return 'Dev Workflows' case 'integrations': - return 'team-api' + return 'API' + case 'security': + return 'Security' case 'platform': case 'monitoring-troubleshooting': - return 'team-infra' + return 'Infra' default: return DEFAULT_TEAM } @@ -51,4 +53,4 @@ const getSanitizedTabParams = () => { return pick(Object.fromEntries(searchParams.entries()), queryGroups) } -export { getNotionTeam, getSanitizedTabParams } +export { getLinearTeam, getSanitizedTabParams } diff --git a/apps/docs/components/JwtGenerator/JwtGenerator.tsx b/apps/docs/components/JwtGenerator/JwtGenerator.tsx index cc767e1d5b3..f7e5132550a 100644 --- a/apps/docs/components/JwtGenerator/JwtGenerator.tsx +++ b/apps/docs/components/JwtGenerator/JwtGenerator.tsx @@ -1,5 +1,5 @@ import { KJUR } from 'jsrsasign' -import { useState, ChangeEvent } from 'react' +import { ChangeEvent, useState } from 'react' import { Button, CodeBlock, Input, Select } from 'ui' const JWT_HEADER = { alg: 'HS256', typ: 'JWT' } @@ -53,7 +53,7 @@ const generateRandomString = (length: number) => { return result } -export default function JwtGenerator({}) { +export default function JwtGenerator() { const secret = generateRandomString(40) const [jwtSecret, setJwtSecret] = useState(secret) @@ -110,6 +110,7 @@ export default function JwtGenerator({}) {
import('./GlobalMobileMenu')) const TopNavDropdown = dynamic(() => import('./TopNavDropdown')) @@ -17,6 +19,8 @@ const TopNavBar: FC = () => { const isLoggedIn = useIsLoggedIn() const isUserLoading = useIsUserLoading() const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + const user = useUser() + const menu = useDropdownMenu(user) return ( <> @@ -95,7 +99,11 @@ const TopNavBar: FC = () => { Dev-only secret sign-in )} - + {isLoggedIn ? ( + + ) : ( + + )}
diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavDropdown.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavDropdown.tsx index 5b102255a38..7d7999c63c8 100644 --- a/apps/docs/components/Navigation/NavigationMenu/TopNavDropdown.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/TopNavDropdown.tsx @@ -19,7 +19,6 @@ import { cn, themes, } from 'ui' - import MenuIconPicker from './MenuIconPicker' const menu = [ @@ -70,7 +69,7 @@ const TopNavDropdown = () => { - + {menu.map((menuSection, sectionIdx) => ( {sectionIdx !== 0 && } diff --git a/apps/docs/components/Navigation/NavigationMenu/useDropdownMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/useDropdownMenu.tsx new file mode 100644 index 00000000000..ab0fb45827e --- /dev/null +++ b/apps/docs/components/Navigation/NavigationMenu/useDropdownMenu.tsx @@ -0,0 +1,80 @@ +'use client' + +import type { User } from '@supabase/supabase-js' +import { LogOut, Globe, LifeBuoy, Settings, UserIcon, Database } from 'lucide-react' +import { logOut } from 'common' + +import type { menuItem } from 'ui-patterns/AuthenticatedDropdownMenu' +import { IconGitHub } from './MenuIcons' + +const useDropdownMenu = (user: User | null) => { + const menu: menuItem[][] = [ + [ + { + label: user?.email ?? "You're logged in", + type: 'text', + icon: UserIcon, + }, + { + label: 'Account Preferences', + icon: Settings, + href: 'https://supabase.com/dashboard/account/me', + }, + { + label: 'All Projects', + icon: Database, + href: 'https://supabase.com/dashboard/projects', + }, + ], + [ + { + label: 'Supabase.com', + icon: Globe, + href: 'https://supabase.com', + otherProps: { + target: '_blank', + rel: 'noreferrer noopener', + }, + }, + { + label: 'GitHub', + icon: IconGitHub as any, + href: 'https://github.com/supabase/supabase', + otherProps: { + target: '_blank', + rel: 'noreferrer noopener', + }, + }, + { + label: 'Support', + icon: LifeBuoy, + href: 'https://supabase.com/support', + otherProps: { + target: '_blank', + rel: 'noreferrer noopener', + }, + }, + ], + [ + { + label: 'Theme', + type: 'theme', + }, + ], + [ + { + label: 'Logout', + type: 'button', + icon: LogOut, + onClick: async () => { + await logOut() + window.location.reload() + }, + }, + ], + ] + + return menu +} + +export default useDropdownMenu diff --git a/apps/docs/components/Price.tsx b/apps/docs/components/Price.tsx new file mode 100644 index 00000000000..3a58f96c69f --- /dev/null +++ b/apps/docs/components/Price.tsx @@ -0,0 +1,7 @@ +interface PriceProps { + price: string +} + +export const Price: React.FC = ({ price }) => { + return ${price} +} diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx index 41250bcb1dc..565fca8b3f2 100644 --- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx +++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx @@ -15,7 +15,7 @@ import CopyToClipboard from 'react-copy-to-clipboard' import { withErrorBoundary } from 'react-error-boundary' import { proxy, useSnapshot } from 'valtio' -import { useIsLoggedIn, useIsUserLoading } from 'common' +import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsUserLoading } from 'common' import { Button_Shadcn_ as Button, Input_Shadcn_ as Input, cn } from 'ui' import { @@ -36,7 +36,7 @@ import { useOrganizationsQuery } from '~/lib/fetch/organizations' import { type SupavisorConfigData, useSupavisorConfigQuery } from '~/lib/fetch/pooler' import { useProjectApiQuery } from '~/lib/fetch/projectApi' import { isProjectPaused, type ProjectsData, useProjectsQuery } from '~/lib/fetch/projects' -import { LOCAL_STORAGE_KEYS, retrieve, storeOrRemoveNull } from '~/lib/storage' +import { retrieve, storeOrRemoveNull } from '~/lib/storage' import { useOnLogout } from '~/lib/userAuth' type ProjectOrgDataState = diff --git a/apps/docs/content/_partials/billing/pricing/pricing_branching.mdx b/apps/docs/content/_partials/billing/pricing/pricing_branching.mdx index 6aa5ca8ed90..ff9420f1ca3 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_branching.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_branching.mdx @@ -1 +1 @@ -There is no fixed fee for a Preview branch. You only pay for the usage it incurs. With Compute costs of $0.01344 per hour, a branch running on Micro Compute size starts at $0.32 per day. +There is no fixed fee for a Preview branch. You only pay for the usage it incurs. With Compute costs of per hour, a branch running on Micro Compute size starts at per day. diff --git a/apps/docs/content/_partials/billing/pricing/pricing_edge_functions.mdx b/apps/docs/content/_partials/billing/pricing/pricing_edge_functions.mdx index 4a68dc3316a..4a6e51cb3b6 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_edge_functions.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_edge_functions.mdx @@ -1,8 +1,9 @@ -$2 per 1 million invocations. You are only charged for usage exceeding your subscription plan's quota. + per 1 million invocations. You are only charged for usage exceeding your subscription +plan's quota. -| Plan | Quota | Over-Usage | -| ---------- | --------- | ---------------------------- | -| Free | 500,000 | - | -| Pro | 2 million | $2 per 1 million invocations | -| Team | 2 million | $2 per 1 million invocations | -| Enterprise | Custom | Custom | +| Plan | Quota | Over-Usage | +| ---------- | --------- | --------------------------------------------- | +| Free | 500,000 | - | +| Pro | 2 million | per 1 million invocations | +| Team | 2 million | per 1 million invocations | +| Enterprise | Custom | Custom | diff --git a/apps/docs/content/_partials/billing/pricing/pricing_mau.mdx b/apps/docs/content/_partials/billing/pricing/pricing_mau.mdx index c3cf0efbc1d..ed4f30f7d9c 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_mau.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_mau.mdx @@ -1 +1,2 @@ -$0.00325 per MAU. You are only charged for usage exceeding your subscription plan's quota. + per MAU. You are only charged for usage exceeding your subscription plan's +quota. diff --git a/apps/docs/content/_partials/billing/pricing/pricing_mau_sso.mdx b/apps/docs/content/_partials/billing/pricing/pricing_mau_sso.mdx index a2a65032667..bf5d2628bd6 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_mau_sso.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_mau_sso.mdx @@ -1 +1,2 @@ -$0.015 per SSO MAU. You are only charged for usage exceeding your subscription plan's quota. + per SSO MAU. You are only charged for usage exceeding your subscription plan's +quota. diff --git a/apps/docs/content/_partials/billing/pricing/pricing_mau_third_party.mdx b/apps/docs/content/_partials/billing/pricing/pricing_mau_third_party.mdx index fe567ae3e55..aa7576751bd 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_mau_third_party.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_mau_third_party.mdx @@ -1 +1,2 @@ -$0.00325 per Third-Party MAU. You are only charged for usage exceeding your subscription plan's quota. + per Third-Party MAU. You are only charged for usage exceeding your subscription +plan's quota. diff --git a/apps/docs/content/_partials/billing/pricing/pricing_mfa_phone.mdx b/apps/docs/content/_partials/billing/pricing/pricing_mfa_phone.mdx index ea50511e1ca..5635b906706 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_mfa_phone.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_mfa_phone.mdx @@ -1,7 +1,8 @@ -$0.1027 per hour ($75 per month) for the first project. $0.0137 per hour ($10 per month) for every additional project. + per hour ( per month) for the first project. per +hour ( per month) for every additional project. -| Plan | Project 1 per month | Project 2 per month | Project 3 per month | -| ---------- | ------------------- | ------------------- | ------------------- | -| Pro | $75 | $10 | $10 | -| Team | $75 | $10 | $10 | -| Enterprise | Custom | Custom | Custom | +| Plan | Project 1 per month | Project 2 per month | Project 3 per month | +| ---------- | -------------------- | -------------------- | -------------------- | +| Pro | | | | +| Team | | | | +| Enterprise | Custom | Custom | Custom | diff --git a/apps/docs/content/_partials/billing/pricing/pricing_pitr.mdx b/apps/docs/content/_partials/billing/pricing/pricing_pitr.mdx index 435f156a2c0..36b2c8801f1 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_pitr.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_pitr.mdx @@ -1,7 +1,7 @@ Pricing depends on the recovery retention period, which determines how many days back you can restore data to any chosen point of up to seconds in granularity. -| Recovery Retention Period in Days | Hourly Price USD | Monthly Price USD | -| --------------------------------- | ---------------- | ----------------- | -| 7 | $0.137 | $100 | -| 14 | $0.274 | $200 | -| 28 | $0.55 | $400 | +| Recovery Retention Period in Days | Hourly Price USD | Monthly Price USD | +| --------------------------------- | ----------------------- | --------------------- | +| 7 | | | +| 14 | | | +| 28 | | | diff --git a/apps/docs/content/_partials/billing/pricing/pricing_realtime_connections.mdx b/apps/docs/content/_partials/billing/pricing/pricing_realtime_connections.mdx index 22a03499d3f..5e8737d0af3 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_realtime_connections.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_realtime_connections.mdx @@ -1,8 +1,9 @@ -$10 per 1,000 peak connections. You are only charged for usage exceeding your subscription plan's quota. + per 1,000 peak connections. You are only charged for usage exceeding your subscription +plan's quota. -| Plan | Quota | Over-Usage | -| ---------- | ------ | ------------------------------ | -| Free | 200 | - | -| Pro | 500 | $10 per 1,000 peak connections | -| Team | 500 | $10 per 1,000 peak connections | -| Enterprise | Custom | Custom | +| Plan | Quota | Over-Usage | +| ---------- | ------ | ----------------------------------------------- | +| Free | 200 | - | +| Pro | 500 | per 1,000 peak connections | +| Team | 500 | per 1,000 peak connections | +| Enterprise | Custom | Custom | diff --git a/apps/docs/content/_partials/billing/pricing/pricing_realtime_messages.mdx b/apps/docs/content/_partials/billing/pricing/pricing_realtime_messages.mdx index 2e64090fd92..3cbea00db4f 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_realtime_messages.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_realtime_messages.mdx @@ -1,8 +1,9 @@ -$2.50 per 1 million messages. You are only charged for usage exceeding your subscription plan's quota. + per 1 million messages. You are only charged for usage exceeding your subscription +plan's quota. -| Plan | Quota | Over-Usage | -| ---------- | --------- | ---------------------------- | -| Free | 2 million | - | -| Pro | 5 million | $2.50 per 1 million messages | -| Team | 5 million | $2.50 per 1 million messages | -| Enterprise | Custom | Custom | +| Plan | Quota | Over-Usage | +| ---------- | --------- | --------------------------------------------- | +| Free | 2 million | - | +| Pro | 5 million | per 1 million messages | +| Team | 5 million | per 1 million messages | +| Enterprise | Custom | Custom | diff --git a/apps/docs/content/_partials/billing/pricing/pricing_storage_image_transformations.mdx b/apps/docs/content/_partials/billing/pricing/pricing_storage_image_transformations.mdx index 0525d0e76a2..4c4d4d429db 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_storage_image_transformations.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_storage_image_transformations.mdx @@ -1,4 +1,5 @@ -$5 per 1,000 origin images. You are only charged for usage exceeding your subscription plan's quota. + per 1,000 origin images. You are only charged for usage exceeding your subscription +plan's quota. @@ -6,8 +7,8 @@ The count resets at the start of each billing cycle. -| Plan | Quota | Over-Usage | -| ---------- | ------ | -------------------------- | -| Pro | 100 | $5 per 1,000 origin images | -| Team | 100 | $5 per 1,000 origin images | -| Enterprise | Custom | Custom | +| Plan | Quota | Over-Usage | +| ---------- | ------ | ------------------------------------------- | +| Pro | 100 | per 1,000 origin images | +| Team | 100 | per 1,000 origin images | +| Enterprise | Custom | Custom | diff --git a/apps/docs/content/_partials/billing/pricing/pricing_storage_size.mdx b/apps/docs/content/_partials/billing/pricing/pricing_storage_size.mdx index 883298c4981..6fb439f74e9 100644 --- a/apps/docs/content/_partials/billing/pricing/pricing_storage_size.mdx +++ b/apps/docs/content/_partials/billing/pricing/pricing_storage_size.mdx @@ -1,8 +1,9 @@ -$0.00002919 per GB-Hr ($0.021 per GB per month). You are only charged for usage exceeding your subscription plan's quota. + per GB-Hr ( per GB per month). You are only +charged for usage exceeding your subscription plan's quota. -| Plan | Quota in GB | Over-Usage per GB | Quota in GB-Hrs | Over-Usage per GB-Hr | -| ---------- | ----------- | ----------------- | --------------- | -------------------- | -| Free | 1 | - | 744 | - | -| Pro | 100 | $0.021 | 74,400 | $0.00002919 | -| Team | 100 | $0.021 | 74,400 | $0.00002919 | -| Enterprise | Custom | Custom | Custom | Custom | +| Plan | Quota in GB | Over-Usage per GB | Quota in GB-Hrs | Over-Usage per GB-Hr | +| ---------- | ----------- | ----------------------- | --------------- | ---------------------------- | +| Free | 1 | - | 744 | - | +| Pro | 100 | | 74,400 | | +| Team | 100 | | 74,400 | | +| Enterprise | Custom | Custom | Custom | Custom | diff --git a/apps/docs/content/guides/auth/auth-helpers/auth-ui.mdx b/apps/docs/content/guides/auth/auth-helpers/auth-ui.mdx index 7b239511062..bda36fa1ae0 100644 --- a/apps/docs/content/guides/auth/auth-helpers/auth-ui.mdx +++ b/apps/docs/content/guides/auth/auth-helpers/auth-ui.mdx @@ -9,6 +9,8 @@ sitemapPriority: 0.5 As of 7th Feb 2024, [this repository](https://github.com/supabase-community/auth-ui) is no longer maintained by the Supabase Team. At the moment, the team does not have capacity to give the expected level of care to this repository. We may revisit Auth UI in the future but regrettably have to leave it on hold for now as we focus on other priorities such as improving the Server-Side Rendering (SSR) package and advanced Auth primitives. +As an alternative you can use the [Supabase UI Library](https://supabase.com/ui) which has auth ready blocks to use in your projects. + Auth UI is a pre-built React component for authenticating users. diff --git a/apps/docs/content/guides/auth/auth-hooks/send-email-hook.mdx b/apps/docs/content/guides/auth/auth-hooks/send-email-hook.mdx index f7a08c4934d..4da5443d9cb 100644 --- a/apps/docs/content/guides/auth/auth-hooks/send-email-hook.mdx +++ b/apps/docs/content/guides/auth/auth-hooks/send-email-hook.mdx @@ -643,7 +643,7 @@ const templates = { function generateConfirmationURL(email_data) { // TODO: replace the ref with your project ref - return `https://.supabase.co/auth/v1/verify?token=${email_data.token}&type=${email_data.email_action_type}&redirect_to=${email_data.redirect_to}` + return `https://.supabase.co/auth/v1/verify?token=${email_data.token_hash}&type=${email_data.email_action_type}&redirect_to=${email_data.redirect_to}` } Deno.serve(async (req) => { @@ -755,7 +755,7 @@ const templates = { function generateConfirmationURL(email_data) { // TODO: replace the ref with your project ref - return `https://.supabase.co/auth/v1/verify?token=${email_data.token}&type=${email_data.email_action_type}&redirect_to=${email_data.redirect_to}` + return `https://.supabase.co/auth/v1/verify?token=${email_data.token_hash}&type=${email_data.email_action_type}&redirect_to=${email_data.redirect_to}` } async function sendEmailWithPostmark(user: any, email_data: any, serverToken: string): Promise { diff --git a/apps/docs/content/guides/auth/password-security.mdx b/apps/docs/content/guides/auth/password-security.mdx index 2b2b0f3a456..4582c5ed77a 100644 --- a/apps/docs/content/guides/auth/password-security.mdx +++ b/apps/docs/content/guides/auth/password-security.mdx @@ -26,6 +26,12 @@ To help protect your users, Supabase Auth allows you fine-grained control over t - Set the required characters that must appear at least once in a user's password. Use the strongest option of requiring digits, lowercase and uppercase letters, and symbols. - Prevent the use of leaked passwords. Supabase Auth uses the open-source [HaveIBeenPwned.org Pwned Passwords API](https://haveibeenpwned.com/Passwords) to reject passwords that have been leaked and are known by malicious actors. + + +Leaked password protection is available on the Pro Plan and above. + + + ## Additional recommendations In addition to choosing suitable password strength settings and preventing the use of leaked passwords, consider asking your users to: diff --git a/apps/docs/content/guides/auth/quickstarts/nextjs.mdx b/apps/docs/content/guides/auth/quickstarts/nextjs.mdx index 90b0bec7d26..69e6a29ef44 100644 --- a/apps/docs/content/guides/auth/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/auth/quickstarts/nextjs.mdx @@ -52,7 +52,7 @@ hideToc: true - Rename `.env.local.example` to `.env.local` and populate with [your project's URL and Anon Key](https://supabase.com/dashboard/project/_/settings/api). + Rename `.env.example` to `.env.local` and populate with [your project's URL and Anon Key](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/docs/content/guides/auth/quickstarts/react-native.mdx b/apps/docs/content/guides/auth/quickstarts/react-native.mdx index 2bae82985e1..ac779e0e5fc 100644 --- a/apps/docs/content/guides/auth/quickstarts/react-native.mdx +++ b/apps/docs/content/guides/auth/quickstarts/react-native.mdx @@ -74,7 +74,7 @@ hideToc: true import { AppState } from 'react-native' import 'react-native-url-polyfill/auto' import AsyncStorage from '@react-native-async-storage/async-storage' - import { createClient } from '@supabase/supabase-js' + import { createClient, processLock } from '@supabase/supabase-js' const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY @@ -85,6 +85,7 @@ hideToc: true autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, + lock: processLock, }, }) diff --git a/apps/docs/content/guides/auth/server-side/nextjs.mdx b/apps/docs/content/guides/auth/server-side/nextjs.mdx index 3f5d6fe41dd..1b3390e20a6 100644 --- a/apps/docs/content/guides/auth/server-side/nextjs.mdx +++ b/apps/docs/content/guides/auth/server-side/nextjs.mdx @@ -565,7 +565,7 @@ Fill in your `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY`: -```txt .env.local +```txt name=.env.local NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= ``` diff --git a/apps/docs/content/guides/auth/social-login/auth-apple.mdx b/apps/docs/content/guides/auth/social-login/auth-apple.mdx index 0316b186396..d7ad99022c3 100644 --- a/apps/docs/content/guides/auth/social-login/auth-apple.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-apple.mdx @@ -14,7 +14,7 @@ To support Sign in with Apple, you need to configure the [Apple provider in the There are three general ways to use Sign in with Apple, depending on the application you're trying to build: - Sign in on the web or in web-based apps - - Using an OAuth flow initiated by Supabase Auth using the [Sign in with Apple REST API](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api). + - Using an OAuth flow initiated by Supabase Auth using the [Sign in with Apple REST API](https://developer.apple.com/documentation/signinwithapplerestapi). - Using [Sign in with Apple JS](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js) directly in the browser, usually suitable for websites. - Sign in natively inside iOS, macOS, watchOS or tvOS apps using [Apple's Authentication Services](https://developer.apple.com/documentation/authenticationservices) @@ -35,7 +35,7 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app, Sign in with Apple's OAuth flow is designed for web or browser based sign in methods. It can be used on web-based apps as well as websites, though some users can benefit by using Sign in with Apple JS directly. - Behind the scenes, Supabase Auth uses the [REST APIs](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api) provided by Apple. + Behind the scenes, Supabase Auth uses the [REST APIs](https://developer.apple.com/documentation/signinwithapplerestapi) provided by Apple. <$Partial path="create_client_snippet.mdx" /> @@ -368,7 +368,7 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app, ) { install(GoTrue) install(ComposeAuth) { - nativeAppleLogin() + appleNativeLogin() } } ``` diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index b5c17ba322e..7dfb9e58196 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -694,9 +694,9 @@ val supabaseClient = createSupabaseClient( supabaseUrl = "SUPABASE_URL", supabaseKey = "SUPABASE_KEY" ) { - install(GoTrue) + install(Auth) install(ComposeAuth) { - nativeGoogleLogin("WEB_GOOGLE_CLIENT_ID") //Use the Web Client ID, not the Android one! + googleNativeLogin("WEB_GOOGLE_CLIENT_ID") //Use the Web Client ID, not the Android one! } } ``` diff --git a/apps/docs/content/guides/auth/third-party/clerk.mdx b/apps/docs/content/guides/auth/third-party/clerk.mdx index 3a422696986..852eb4dedc2 100644 --- a/apps/docs/content/guides/auth/third-party/clerk.mdx +++ b/apps/docs/content/guides/auth/third-party/clerk.mdx @@ -37,15 +37,11 @@ If you are not able to use [Clerk's Connect with Supabase page](https://dashboar -```typescript -import { createClient } from '@supabase/supabase-js' - -const supabase = createClient('https://.supabase.co', 'SUPABASE_ANON_KEY', { - accessToken: () => { - return Clerk.session?.getToken() - }, -}) -``` +<$CodeSample +path="/clerk/hooks/useSupabaseClient.ts" +lines={[[6, 14]]} +hideElidedLines={true} +/> @@ -99,34 +95,23 @@ The recommended way to design RLS policies with Clerk is to use claims present i ### Example: Check user organization role -```sql -create policy "Only organization admins can insert in table" - on table_name - for insert - to authenticated - with check ( - ((select auth.jwt()->>'org_role') = 'org:admin') - and - (organization_id = (select auth.jwt()->>'org_id')) - ); -``` +<$CodeSample +path="/clerk/supabase/migrations/20250501155648_setup_database.sql" +lines={[[10, 18]]} +hideElidedLines={true} +/> -This RLS policy checks that the newly inserted row in the table has the user's declared organization ID (from the `org_id` session token claim) in the `organization_id` column. Additionally it ensures that they're an `org:admin`. +This RLS policy checks that the newly inserted row in the table has the user's declared organization ID in the `organization_id` column. Additionally it ensures that they're an `org:admin`. This way only organization admins can add rows to the table, for organizations they're a member of. ### Example: Check user has passed second factor verification -```sql -create policy "Only users that have passed second factor verification can read from table" - on table_name - for select - as restrictive - to authenticated - using ( - ((select auth.jwt()->'fva'->>1) != '-1') - ); -``` +<$CodeSample +path="/clerk/supabase/migrations/20250501155648_setup_database.sql" +lines={[[28, 35]]} +hideElidedLines={true} +/> This example uses a restrictive RLS policy checks that the [second factor verification](https://clerk.com/docs/guides/reverification) age element in the `fva` claim is not `'-1'` indicating the user has passed through second factor verification. diff --git a/apps/docs/content/guides/cron.mdx b/apps/docs/content/guides/cron.mdx index 4682251b0d7..9d4d1a1b29e 100644 --- a/apps/docs/content/guides/cron.mdx +++ b/apps/docs/content/guides/cron.mdx @@ -5,7 +5,15 @@ subtitle: 'Schedule Recurring Jobs with Cron Syntax in Postgres' Supabase Cron is a Postgres Module that simplifies scheduling recurring Jobs with cron syntax and monitoring Job runs inside Postgres. -Cron Jobs can be created via SQL or the Cron interface inside of Supabase Dashboard and can run anywhere from every second to once a year depending on your use case. +Cron Jobs can be created via SQL or the [Integrations -> Cron](https://supabase.com/dashboard/project/_/integrations) interface inside the Dashboard, and can run anywhere from every second to once a year depending on your use case. + +Manage cron jobs via the Dashboard Every Job can run SQL snippets or database functions with zero network latency or make an HTTP request, such as invoking a Supabase Edge Function, with ease. diff --git a/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx b/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx index fe490755a0b..17e8782a234 100644 --- a/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx +++ b/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx @@ -8,19 +8,9 @@ description: 'Block queries over a total cost limit' ## Enable the extension -`pg_plan_filter` can be enabled on a per connection basis: +The extension is already enabled by default via `shared_preload_libraries` setting. -{/* prettier-ignore */} -```sql -load 'plan_filter'; -``` - -or for all connections: - -{/* prettier-ignore */} -```sql -alter database some_db set session_preload_libraries = 'plan_filter'; -``` +You can follow the instructions below. ## API @@ -66,7 +56,6 @@ Now we can choose a `statement_cost_filter` value between the total cost for the {/* prettier-ignore */} ```sql -load 'plan_filter'; set plan_filter.statement_cost_limit = 50; -- between 2.49 and 135.0 select * from book where id = 1; diff --git a/apps/docs/content/guides/database/extensions/timescaledb.mdx b/apps/docs/content/guides/database/extensions/timescaledb.mdx index 268a5006c80..fa75a8d0c68 100644 --- a/apps/docs/content/guides/database/extensions/timescaledb.mdx +++ b/apps/docs/content/guides/database/extensions/timescaledb.mdx @@ -35,7 +35,7 @@ Supabase projects come with [TimescaleDB Apache 2 Edition](https://docs.timescal {/* prettier-ignore */} ```sql -- Enable the "timescaledb" extension -create extension timescaledb; +create extension timescaledb with schema extensions; -- Disable the "timescaledb" extension drop extension if exists timescaledb; @@ -44,6 +44,10 @@ drop extension if exists timescaledb; +Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension". To disable an extension you can call `drop extension`. + +It's good practice to create the extension within a separate schema (like `extensions`) to keep your `public` schema clean. + ## Usage To demonstrate how `timescaledb` works, let's consider a simple example where we have a table that stores temperature data from different sensors. We will create a table named "temperatures" and store data for two sensors. diff --git a/apps/docs/content/guides/database/postgres/configuration.mdx b/apps/docs/content/guides/database/postgres/configuration.mdx index 2146c516fea..ca8ff63297e 100644 --- a/apps/docs/content/guides/database/postgres/configuration.mdx +++ b/apps/docs/content/guides/database/postgres/configuration.mdx @@ -20,8 +20,13 @@ This data can further be used in conjunction with the [`explain`](https://www.po ## Managing timezones -Every Supabase database is set to UTC timezone by default. We strongly recommend keeping it this way, even if your users are in a different location. -This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that everything in your database is in UTC time. +Every hosted Supabase database is set to UTC timezone by default. We strongly recommend keeping it this way, even if your users are in a different location. This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that everything in your database is in UTC time. + + + +On self-hosted databases, the timezone defaults to your local timezone. We recommend [changing this to UTC](/docs/guides/database/postgres/configuration#change-timezone) for the same reasons. + + ### Change timezone diff --git a/apps/docs/content/guides/deployment/branching.mdx b/apps/docs/content/guides/deployment/branching.mdx index 1f4e2a71cb1..ce14f125938 100644 --- a/apps/docs/content/guides/deployment/branching.mdx +++ b/apps/docs/content/guides/deployment/branching.mdx @@ -624,7 +624,7 @@ You can make a similar setup with a distinct project for each environment. Or ju Branching is available on the Pro Plan and above. The price is: -- Each Preview branch costs $0.32 per day +- Each Preview branch costs per day - Each Preview branch is billed until it is removed ## Troubleshooting diff --git a/apps/docs/content/guides/functions/websockets.mdx b/apps/docs/content/guides/functions/websockets.mdx index 83e134e4421..1a4762e1d22 100644 --- a/apps/docs/content/guides/functions/websockets.mdx +++ b/apps/docs/content/guides/functions/websockets.mdx @@ -236,7 +236,6 @@ To prevent that, you can update the `supabase/config.toml` with the following se ```toml [edge_runtime] policy = "per_worker" -``` +```` When running with `per_worker` policy, Function won't auto-reload on edits. You will need to manually restart it by running `supabase functions serve`. -```` diff --git a/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx index 6e19c64f2ed..bfc6a12f4cc 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx @@ -50,15 +50,46 @@ hideToc: true - - + + - In `app.vue`, create a Supabase client using your project URL and public API (anon) key: + Create a `.env` file and populate with your Supabase connection variables: - Replace the existing content in your `app.vue` file with the following code. + + + + + <$CodeTabs> + + ```text name=.env.local + SUPABASE_URL= + SUPABASE_ANON_KEY= + ``` + + ```ts name=nuxt.config.tsx + export default defineNuxtConfig({ + runtimeConfig: { + public: { + supabaseUrl: process.env.SUPABASE_URL, + supabaseAnonKey: process.env.SUPABASE_ANON_KEY, + }, + }, + }); + ``` + + + + + + + + + + + In `app.vue`, create a Supabase client using your config values and replace the existing content with the following code. @@ -67,7 +98,8 @@ hideToc: true ```vue name=app.vue