diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 92882cec376..5b3982ca695 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,6 @@ /packages/shared-data/pricing.ts @roryw10 @kevcodez /packages/shared-data/plans.ts @roryw10 @kevcodez +/packages/common/telemetry-constants.ts @4L3k51 @loong @pamelachia /apps/studio/ @supabase/Dashboard @@ -17,5 +18,3 @@ /apps/studio/components/interfaces/Organization/BillingSettings/ @supabase/security /apps/studio/components/interfaces/Organization/Documents/ @supabase/security /apps/studio/pages/new/index.tsx @supabase/security - -/apps/studio/lib/constants/telemetry.ts @4L3k51 @loong @pamelachia \ No newline at end of file diff --git a/.github/workflows/docs-sync-troubleshooting.yml b/.github/workflows/docs-sync-troubleshooting.yml index 0b2d3d04e86..f0d2ec07116 100644 --- a/.github/workflows/docs-sync-troubleshooting.yml +++ b/.github/workflows/docs-sync-troubleshooting.yml @@ -29,17 +29,22 @@ jobs: sparse-checkout: | apps/docs - - name: Set up Node.js + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + - name: Use Node.js uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' - cache: 'npm' + cache: 'pnpm' - name: Install dependencies - run: npm ci + run: pnpm i - name: Run Troubleshooting script - run: npm --prefix=apps/docs run troubleshooting:sync + run: pnpm run -F docs troubleshooting:sync - name: Create Pull Request uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5 diff --git a/.github/workflows/playwright.yml b/.github/workflows/local-studio-tests.yml similarity index 81% rename from .github/workflows/playwright.yml rename to .github/workflows/local-studio-tests.yml index 015031e3153..9c64715b1e0 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/local-studio-tests.yml @@ -1,14 +1,16 @@ -name: Playwright Tests +name: Local Studio Tests on: push: branches: [master] paths: - 'apps/studio/**' + - 'tests/local-studio-tests/**' - 'pnpm-lock.yaml' pull_request: branches: [master] paths: - 'apps/studio/**' + - 'tests/local-studio-tests/**' - 'pnpm-lock.yaml' # Cancel old builds on new commit for same workflow + branch/PR @@ -37,14 +39,14 @@ jobs: - name: Install dependencies run: pnpm i - name: Install Playwright Browsers - run: pnpm -C playwright-tests exec playwright install --with-deps + run: pnpm -C tests/local-studio-tests exec playwright install --with-deps - name: Run Playwright tests - run: pnpm run test:playwright + run: pnpm test:local-studio # mark the action as succeeded even if the tests failed. This is temporarily until we make the tests more stable. # continue-on-error: true - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report - path: playwright-tests/playwright-report/ + path: tests/local-studio-tests/playwright-report/ retention-days: 7 diff --git a/.prettierignore b/.prettierignore index f399c6bc555..4c3f5a35591 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,13 +9,12 @@ apps/**/out apps/www/schema.sql apps/www/public/images/* apps/docs/**/generated/* +apps/docs/examples examples/slack-clone/nextjs-slack-clone/full-schema.sql # ignore files with custom js formatting -apps/docs/pages/guides/auth/*.mdx -apps/docs/pages/guides/integrations/*.mdx apps/studio/public apps/**/.turbo apps/docs/CONTRIBUTING.md apps/design-system/__registry__ packages/icons/__registry__ -packages/icons/src/icons/*.ts \ No newline at end of file +packages/icons/src/icons/*.ts diff --git a/README.md b/README.md index 437878278b6..3a165a26d6f 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,10 @@ You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and - [Postgres](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance. - [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients. -- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API +- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API. - [GoTrue](https://github.com/supabase/gotrue) is a JWT based API for managing users and issuing JWT tokens. - [Storage](https://github.com/supabase/storage-api) provides a RESTful interface for managing Files stored in S3, using Postgres to manage permissions. -- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API +- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API. - [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc. - [Kong](https://github.com/Kong/kong) is a cloud-native API gateway. diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index ba7caca35fa..8d55cc7b07a 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -2161,6 +2161,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "logs-bar-chart": { + name: "logs-bar-chart", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/logs-bar-chart")), + source: "", + files: ["registry/default/example/logs-bar-chart.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "chart-area-axes": { name: "chart-area-axes", type: "components:block", diff --git a/apps/design-system/app/Providers.tsx b/apps/design-system/app/Providers.tsx index be3bdf78611..87bac1f1e2f 100644 --- a/apps/design-system/app/Providers.tsx +++ b/apps/design-system/app/Providers.tsx @@ -1,17 +1,16 @@ 'use client' -import * as React from 'react' import { Provider as JotaiProvider } from 'jotai' import { ThemeProvider as NextThemesProvider } from 'next-themes' import { ThemeProviderProps } from 'next-themes/dist/types' -import { TooltipProvider_Shadcn_ } from 'ui' +import { TooltipProvider } from 'ui' export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return ( - {children} + {children} ) diff --git a/apps/design-system/config/docs.ts b/apps/design-system/config/docs.ts index fa2e91a1c20..471a9c89158 100644 --- a/apps/design-system/config/docs.ts +++ b/apps/design-system/config/docs.ts @@ -117,6 +117,11 @@ export const docsConfig: DocsConfig = { href: '/docs/fragments/multi-select', items: [], }, + { + title: 'Logs Bar Chart', + href: '/docs/fragments/logs-bar-chart', + items: [], + }, ], }, { diff --git a/apps/design-system/content/docs/fragments/logs-bar-chart.mdx b/apps/design-system/content/docs/fragments/logs-bar-chart.mdx new file mode 100644 index 00000000000..7127ecd2eac --- /dev/null +++ b/apps/design-system/content/docs/fragments/logs-bar-chart.mdx @@ -0,0 +1,21 @@ +--- +title: Logs Bar Chart +description: A stacked bar chart that displays logs errors and successes. +fragment: true +--- + + + +## Usage + +This component is used to display the bar chart in logs pages. + +It requires that the `data` prop is an array of objects with the following shape: + +```ts +type LogsBarChartDatum = { + timestamp: string + ok_count: number + error_count: number +} +``` diff --git a/apps/design-system/registry/default/example/logs-bar-chart.tsx b/apps/design-system/registry/default/example/logs-bar-chart.tsx new file mode 100644 index 00000000000..e639f355f1a --- /dev/null +++ b/apps/design-system/registry/default/example/logs-bar-chart.tsx @@ -0,0 +1,21 @@ +import { LogsBarChart } from 'ui-patterns/LogsBarChart' + +export default function LogsBarChartDemo() { + const data = Array.from({ length: 100 }, (_, i) => { + const date = new Date() + date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart + + return { + timestamp: date.toISOString(), + ok_count: Math.floor(Math.random() * 100), // Random value 0-99 + error_count: Math.floor(Math.random() * 50), // Random value 0-50 + warning_count: Math.floor(Math.random() * 50), // Random value 0-50 + } + }).reverse() + + return ( +
+ +
+ ) +} diff --git a/apps/design-system/registry/default/example/tooltip-demo.tsx b/apps/design-system/registry/default/example/tooltip-demo.tsx index 5584ba3f170..f347278c438 100644 --- a/apps/design-system/registry/default/example/tooltip-demo.tsx +++ b/apps/design-system/registry/default/example/tooltip-demo.tsx @@ -1,22 +1,16 @@ -import { Button } from 'ui' -import { - Tooltip_Shadcn_, - TooltipContent_Shadcn_, - TooltipProvider_Shadcn_, - TooltipTrigger_Shadcn_, -} from 'ui' +import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui' export default function TooltipDemo() { return ( - - - + + + - - + +

Add to library

-
-
-
+ + + ) } diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index eab719afe0e..58d7c1dd508 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -1201,4 +1201,9 @@ export const examples: Registry = [ type: 'docs:example', files: ['example/expanding-textarea-demo.tsx'], }, + { + name: 'logs-bar-chart', + type: 'components:example', + files: ['example/logs-bar-chart.tsx'], + }, ] diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore index 2c0147b651a..d3ed2ff3c4d 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -26,3 +26,6 @@ yarn-error.log* # Sitemap public/sitemap.xml + +# Copied examples folder +/examples/ diff --git a/apps/docs/app/guides/(with-sidebar)/graphql/[[...slug]]/page.tsx b/apps/docs/app/guides/(with-sidebar)/graphql/[[...slug]]/page.tsx index aaf53857d1f..e45d272a948 100644 --- a/apps/docs/app/guides/(with-sidebar)/graphql/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/(with-sidebar)/graphql/[[...slug]]/page.tsx @@ -5,7 +5,7 @@ import rehypeSlug from 'rehype-slug' import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' -import { fetchRevalidatePerDay } from '~/features/helpers.fetch' +import { fetchRevalidatePerDay_TEMP_TESTING } from '~/features/helpers.fetch' import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' @@ -133,7 +133,7 @@ const getContent = async ({ slug }: Params) => { const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`) - const response = await fetchRevalidatePerDay( + const response = await fetchRevalidatePerDay_TEMP_TESTING( `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}` ) diff --git a/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.Warnings.tsx b/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.Warnings.tsx index 4043d3432e6..d1bb35b32f5 100644 --- a/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.Warnings.tsx +++ b/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.Warnings.tsx @@ -5,12 +5,20 @@ const CostWarning = () => (

To keep SMS sending costs under control, make sure you adjust your project's rate limits - and configure CAPTCHA. See the{' '} - Production Checklist to learn more. + and{' '} + + configure CAPTCHA + + . See the{' '} + + Production Checklist + {' '} + to learn more.

+

- Some countries have special regulations for services that send SMS messages to users, for - example, India's TRAI DLT regulations. Remember to look up and follow the regulations of + Some countries have special regulations for services that send SMS messages to users, (e.g + India's TRAI DLT regulations). Remember to look up and follow the regulations of countries where you operate.

diff --git a/apps/docs/components/Feedback/Feedback.tsx b/apps/docs/components/Feedback/Feedback.tsx index b6928f81971..f16063a9209 100644 --- a/apps/docs/components/Feedback/Feedback.tsx +++ b/apps/docs/components/Feedback/Feedback.tsx @@ -13,6 +13,7 @@ import { } from 'react' import { type Database, useConstant, useIsLoggedIn } from 'common' +import { TelemetryActions } from 'common/telemetry-constants' import { Button, cn } from 'ui' import { IS_PLATFORM } from '~/lib/constants' @@ -108,9 +109,8 @@ function Feedback({ className }: { className?: string }) { function handleVote(response: Response) { sendTelemetryEvent({ - category: 'docs', - action: 'feedback_voted', - label: response, + action: TelemetryActions.DOCS_FEEDBACK_CLICKED, + properties: { response }, }) sendFeedbackVote(response) dispatch({ event: 'VOTED', response }) diff --git a/apps/docs/components/MDX/oauth_pkce_flow.mdx b/apps/docs/components/MDX/oauth_pkce_flow.mdx index 02beff87623..707c0e276c1 100644 --- a/apps/docs/components/MDX/oauth_pkce_flow.mdx +++ b/apps/docs/components/MDX/oauth_pkce_flow.mdx @@ -179,7 +179,6 @@ export async function loader({ request }: LoaderFunctionArgs) { const headers = new Headers() if (code) { - const cookies = parse(request.headers.get('Cookie') ?? '') const supabase = createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, { cookies: { getAll() { diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 7d1ca8e9e5d..59d581a6436 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -212,7 +212,8 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ label: 'Resources', menuItems: [ [ - /* { + /* + { label: 'Troubleshooting', icon: 'contributing', href: '/guides/troubleshooting', @@ -1322,7 +1323,7 @@ export const functions: NavMenuConstant = { name: 'Guides', url: undefined, items: [ - { name: 'Managing dependencies', url: '/guides/functions/import-maps' }, + { name: 'Managing dependencies', url: '/guides/functions/dependencies' }, { name: 'Managing environment variables', url: '/guides/functions/secrets', @@ -1359,6 +1360,10 @@ export const functions: NavMenuConstant = { name: 'Running AI Models', url: '/guides/functions/ai-models', }, + { + name: 'Wasm modules', + url: '/guides/functions/wasm', + }, { name: 'Deploying with CI / CD pipelines', url: '/guides/functions/cicd-workflow', diff --git a/apps/docs/content/guides/auth/auth-hooks/custom-access-token-hook.mdx b/apps/docs/content/guides/auth/auth-hooks/custom-access-token-hook.mdx index 70f151d9f52..a689d23dd79 100644 --- a/apps/docs/content/guides/auth/auth-hooks/custom-access-token-hook.mdx +++ b/apps/docs/content/guides/auth/auth-hooks/custom-access-token-hook.mdx @@ -10,8 +10,8 @@ Claims returned must conform to our specification. Supabase Auth will check for These are the fields currently available on an access token: -Required Claims: `aud`, `exp`, `iat`, `sub`, `email`, `phone`, `role`, `aal`, `session_id` -Optional Claims: `jti`, `iss`, `nbf`, `app_metadata`, `user_metadata`, `amr` +Required Claims: `iss`, `aud`, `exp`, `iat`, `sub`, `role`, `aal`, `session_id` +Optional Claims: `jti`, `nbf`, `app_metadata`, `user_metadata`, `amr`, `email`, `phone` **Inputs** @@ -205,8 +205,57 @@ Return these only if your hook processed the input without errors. scrollable size="small" type="underlined" - defaultActiveId="add-admin-role" + defaultActiveId="minimal-jwt" > + + +Sometimes the size of the JWT can be a problem especially if you're using a [Server-Side Rendering framework](/docs/guides/auth/server-side). Common situations where the JWT can get too large include: + +- The user has a particularly large name, email address or phone number +- The default JWT has too many claims coming from OAuth providers +- A large avatar URL is included + +To lower the size of the JWT you can define a Custom Access Token hook like the one below which will instruct the Auth server to issue a JWT with only the listed claims. Check the documentation above on what JWT claims must be present and cannot be removed. + +Refer to the [Postgres JSON functions](https://www.postgresql.org/docs/current/functions-json.html) on how to manipulate `jsonb` objects. + +```sql +create or replace function public.custom_access_token_hook(event jsonb) +returns jsonb +language plpgsql +as $$ + declare + original_claims jsonb; + new_claims jsonb; + claim text; + begin + original_claims = event->'claims'; + new_claims = '{}'::jsonb; + + foreach claim in array array[ + -- add claims you want to keep here + 'iss', + 'aud', + 'exp', + 'iat', + 'sub', + 'role', + 'aal', + 'session_id' + ] loop + if original_claims ? claim then + -- original_claims contains one of the listed claims, set it on new_claims + new_claims = jsonb_set(new_claims, array[claim], original_claims->claim); + end if; + end loop; + + return jsonb_build_object('claims', new_claims); + end +$$; +``` + + + You can allow registered admin users to perform restricted actions by granting an `admin` claim to their token. diff --git a/apps/docs/content/guides/auth/phone-login.mdx b/apps/docs/content/guides/auth/phone-login.mdx index e24010b1b19..f0b72198d94 100644 --- a/apps/docs/content/guides/auth/phone-login.mdx +++ b/apps/docs/content/guides/auth/phone-login.mdx @@ -3,7 +3,13 @@ title: 'Phone Login' description: 'Learn about logging in to your platform using SMS one-time passwords.' --- -Phone Login is a method of authentication that allows users to log in to a website or application without using a password. The user authenticates through a one-time code sent via SMS. +Phone Login is a method of authentication that allows users to log in to a website or application without using a password. The user authenticates through a one-time password (OTP) sent via a channel (SMS or WhatsApp). + + + +At this time, `WhatsApp` is only supported as a channel for the Twilio and Twilio Verify Providers. + + Users can also log in with their phones using Native Mobile Login with the built-in identity provider. For Native Mobile Login with Android and iOS, see the [Social Login guides](/docs/guides/auth/social-login). diff --git a/apps/docs/content/guides/auth/social-login/auth-azure.mdx b/apps/docs/content/guides/auth/social-login/auth-azure.mdx index 47f6b95dfb6..6e9161a7d7e 100644 --- a/apps/docs/content/guides/auth/social-login/auth-azure.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-azure.mdx @@ -24,7 +24,7 @@ Setting up OAuth with Azure consists of four broad steps: - Under Microsoft Entra ID, select _App registrations_ in the side panel and select _New registration._ - Choose a name and select your preferred option for the supported account types. -- Specify a _Web_ _Redirect URI_. It should should look like this: `https://.supabase.co/auth/v1/callback` +- Specify a _Web_ _Redirect URI_. It should look like this: `https://.supabase.co/auth/v1/callback` - Finally, select _Register_ at the bottom of the screen. ![Register an application.](/docs/img/guides/auth-azure/azure-register-app.png) 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 eaa07a8ecaa..f9c814c232a 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -778,3 +778,11 @@ chrome.identity.launchWebAuthFlow( + +## Google consent screen + +![Google Consent Screen](/docs/img/guides/auth-google/auth-google-consent-screen.png) + +By default, the Google consent screen shows the root domain of the callback URL, where Google will send the authentication response. With Supabase Auth, it is your Supabase project's domain `(https://.supabase.co)`. + +If that is not preferable, you can use a [Custom Domain](https://supabase.com/docs/guides/platform/custom-domains) with your Supabase project. You can use it as your project's domain when creating the Supabase client in your application and initiating the authentication flow. It will then show up in the Google consent screen. If you want your app name and the logo on the consent screen, [you must submit your app to Google for verification](https://support.google.com/cloud/answer/10311615). diff --git a/apps/docs/content/guides/auth/third-party/firebase-auth.mdx b/apps/docs/content/guides/auth/third-party/firebase-auth.mdx index f3762f9476e..0bf453afd5d 100644 --- a/apps/docs/content/guides/auth/third-party/firebase-auth.mdx +++ b/apps/docs/content/guides/auth/third-party/firebase-auth.mdx @@ -4,19 +4,13 @@ title: 'Firebase Auth' subtitle: 'Use Firebase Auth with your Supabase project' --- - - -Creating a Firebase Auth third-party integration with your Supabase project is currently limited to a private alpha release. We are still trying to improve the developer experience around securing your project. [Register your interest in Third-Party Auth with Firebase](https://forms.supabase.com/third-party-auth-with-firebase) and the team will reach out to you. - - - Firebase Auth can be used as a third-party authentication provider alongside Supabase Auth, or standalone, with your Supabase project. ## Getting started 1. First you need to add an integration to connect your Supabase project with your Firebase project. You will need to get the Project ID in the [Firebase Console](https://console.firebase.google.com/u/0/project/_/settings/general). 2. Add a new Third-party Auth integration in your project's [Authentication settings](/dashboard/project/_/settings/auth). -3. If you are using Third Party Auth in local development, create and attach restrictive RLS policies to all tables in your public schema, Storage and Realtime to prevent unauthorized access from unrelated Firebase projects. +3. If you are using Third Party Auth when self hosting, create and attach restrictive RLS policies to all tables in your public schema, Storage and Realtime to **prevent unauthorized access from unrelated Firebase projects**. 4. Assign the `role: 'authenticated'` [custom user claim](https://firebase.google.com/docs/auth/admin/custom-claims) to all your users. 5. Finally set up the Supabase client in your application. @@ -33,7 +27,7 @@ import { createClient } from '@supabase/supabase-js' const supabase = createClient('https://.supabase.co', 'SUPABASE_ANON_KEY', { accessToken: async () => { - ;(await firebase.auth().currentUser?.getIdToken(/* forceRefresh */ false)) ?? null + return await firebase.auth().currentUser?.getIdToken(/* forceRefresh */ false)) ?? null }, }) ``` @@ -123,19 +117,21 @@ enabled = true project_id = "" ``` -## Adding an extra layer of security to your project's RLS policies +## Adding an extra layer of security to your project's RLS policies (self-hosting only) - + -While Third-Party Auth with Firebase is in a private alpha, it is strongly recommended to follow this guide closely. Once it becomes generally available this method would not be necessary to secure your project against unauthorized access from other Firebase projects. +**Please follow this section carefully to prevent unauthorized access to your project's data when self-hosting.** -You should also follow this best practice if self-hosting. +When using the Supabase hosted platform, following this step is optional. -Firebase Auth uses a single set of JWT signing keys for all projects. This means that JWTs issued from an unrelated Firebase project to yours could access data in your Supabase project. To guard against this, creating and maintaining the following RLS policies for **all of your tables in the `public` schema** is very important. You should also attach this policy to [Storage](/docs/guides/storage/security/access-control) buckets or [Realtime](/docs/guides/realtime/authorization) channels. +Firebase Auth uses a single set of JWT signing keys for all projects. This means that JWTs issued from an unrelated Firebase project to yours could access data in your Supabase project. -To achieve this we recommend using a [restrictive Postgres Row-Level Security policy](https://www.postgresql.org/docs/current/sql-createpolicy.html). +When using the Supabase hosted platform, JWTs coming from Firebase project IDs you have not registered will be rejected before they reach your database. When self-hosting implementing this mechanism is your responsibility. An easy way to guard against this is to create and maintain the following RLS policies for **all of your tables in the `public` schema**. You should also attach this policy to [Storage](/docs/guides/storage/security/access-control) buckets or [Realtime](/docs/guides/realtime/authorization) channels. + +It's recommended you use a [restrictive Postgres Row-Level Security policy](https://www.postgresql.org/docs/current/sql-createpolicy.html). Restrictive RLS policies differ from regular (or permissive) policies in that they use the `as restrictive` clause when being defined. They do not grant permissions, but rather restrict any existing or future permissions. They're great for cases like this where the technical limitations of Firebase Auth remain separate from your app's logic. diff --git a/apps/docs/content/guides/database/custom-postgres-config.mdx b/apps/docs/content/guides/database/custom-postgres-config.mdx index 59a41642bdd..b5137339fd2 100644 --- a/apps/docs/content/guides/database/custom-postgres-config.mdx +++ b/apps/docs/content/guides/database/custom-postgres-config.mdx @@ -120,9 +120,12 @@ The following parameters are available for overrides: 1. [max_worker_processes](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-WORKER-PROCESSES) (CLI only) 1. [session_replication_role](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE) 1. [shared_buffers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS) (CLI only) +1. [statement_timeout](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT) +1. [track_activity_query_size](https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-ACTIVITY-QUERY-SIZE) +1. [track_commit_timestamp](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-TRACK-COMMIT-TIMESTAMP) 1. [wal_keep_size](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-KEEP-SIZE) (CLI only) 1. [wal_sender_timeout](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT) (CLI only) -1. [work_mem](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT) +1. [work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM) #### Managing Postgres configuration with the CLI @@ -148,6 +151,22 @@ postgres-config update --config max_parallel_workers=3 \ --replace-existing-overrides ``` +To delete specific configuration overrides, use the `postgres-config delete` command: + +```bash +supabase --experimental \ +--project-ref \ +postgres-config delete --config shared_buffers,work_mem +``` + +By default, this might restart the database after deleting the configurations. You can use the `--no-restart` flag to prevent the restart: + +```bash +supabase --experimental \ +--project-ref \ +postgres-config delete --config shared_buffers --no-restart +``` + ### Resetting to default config To reset a setting to its default value at the database level: @@ -168,6 +187,6 @@ alter role "" set "" to default; ### Considerations -1. Changes through the CLI must restart the database and will cause momentary disruption to existing database connections; in most cases this should not take more than a few seconds. However, you can use the --no-restart flag to bypass the restart and keep the connections intact. Keep in mind that this depends on the specific configuration changes you’re making. if the change requires a restart, using the --no-restart flag will prevent the restart but you won’t see those changes take effect until a restart is manually triggered. +1. Changes through the CLI might restart the database causing momentary disruption to existing database connections; in most cases this should not take more than a few seconds. However, you can use the --no-restart flag to bypass the restart and keep the connections intact. Keep in mind that this depends on the specific configuration changes you're making. if the change requires a restart, using the --no-restart flag will prevent the restart but you won't see those changes take effect until a restart is manually triggered. 1. Custom Postgres Config will always override the default optimizations generated by Supabase. When changing compute add-ons, you should also review and update your custom Postgres Config to ensure they remain compatible and effective with the updated compute. 1. Some parameters (e.g. `wal_keep_size`) can increase disk utilization, triggering disk expansion, which in turn can lead to [increases in your bill](/docs/guides/platform/compute-add-ons#disk-io). diff --git a/apps/docs/content/guides/database/prisma.mdx b/apps/docs/content/guides/database/prisma.mdx index 01755990b0d..2f95850a507 100644 --- a/apps/docs/content/guides/database/prisma.mdx +++ b/apps/docs/content/guides/database/prisma.mdx @@ -140,7 +140,7 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t In your .env file, set the DATABASE_URL variable to your connection string ```text .env # Used for Prisma Migrations and within your application - DATABASE_URL="postgres://[DB-USER].[PROJECT-REF]:[PRISMA-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres" + DATABASE_URL="postgres://[DB-USER].[PROJECT-REF]:[PRISMA-PASSWORD]@[DB-REGION].pooler.supabase.com:5432/postgres" ``` Change your string's `[DB-USER]` to `prisma` and add the password you created in step 1 diff --git a/apps/docs/content/guides/deployment/branching.mdx b/apps/docs/content/guides/deployment/branching.mdx index d296d042eb6..1eec46d5754 100644 --- a/apps/docs/content/guides/deployment/branching.mdx +++ b/apps/docs/content/guides/deployment/branching.mdx @@ -24,7 +24,14 @@ If you understand Git, you already understand Supabase Branching. - **Supabase Project**: You need an existing Supabase project. - **GitHub Repository**: Your project must be connected to a GitHub repository containing your Supabase directory. -You can run multiple Preview Branches for every Supabase project. Branches contain all the Supabase features with their own API credentials. Preview Environments pause automatically after branching.inactivity_period_in_minutes minutes of inactivity. Note that `pg_cron` executions will be impacted by inactivity related pausing. +You can run multiple Preview Branches for every Supabase project. Branches contain all the Supabase features with their own API credentials. + +Preview Environments auto-pause after branching.inactivity_period_in_minutes minutes of inactivity. Upon receiving a new request to your database or REST API, the paused branch will automatically resume to serve the request. The implications of this architecture means + +- `pg_cron` jobs will not execute in an auto-paused database. +- Larger variance in request latency due to database cold starts. + +If you need higher performance guarantees on your Preview Environment, you can switch individual branches to [persistent](/docs/guides/deployment/branching#persistent-branches) so they are not auto-paused. -Supabase automatically updates your Vercel project with the correct environment variables for the corresponding preview branches. -The synchronization happens at the time of Pull Request being opened, not at the time of branch creation. +And make sure you have [connected](/dashboard/org/_/integrations) your Supabase project to your Vercel project. + +Supabase automatically updates your Vercel project with the correct environment variables for the corresponding preview branches. The synchronization happens at the time of Pull Request being opened, not at the time of branch creation. + As branching integration is tied to the Preview Deployments feature in Vercel, there are possible race conditions between Supabase setting correct variables, and Vercel running a deployment process. Because of that, Supabase is always automatically re-deploying the most recent deployment of the given pull request. ## Other Git providers @@ -613,7 +622,7 @@ The new preview branch is reseeded from the `./supabase/seed.sql` file by defaul ### Deployment failures -A deployment might fail for various reasons, including invalid SQL statements and schema conflicts in migrations. Errors within the `config.toml` config or something else. +A deployment might fail for various reasons, including invalid SQL statements and schema conflicts in migrations, errors within the `config.toml` config, or something else. To check the error message, see the Supabase workflow run for your branch under the [View logs](/dashboard/project/_/branches) section. diff --git a/apps/docs/content/guides/functions/auth.mdx b/apps/docs/content/guides/functions/auth.mdx index 639e667ff34..7b97b497278 100644 --- a/apps/docs/content/guides/functions/auth.mdx +++ b/apps/docs/content/guides/functions/auth.mdx @@ -16,12 +16,15 @@ import { createClient } from 'jsr:@supabase/supabase-js@2' Deno.serve(async (req: Request) => { - const authHeader = req.headers.get('Authorization')! const supabaseClient = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', - { global: { headers: { Authorization: authHeader } } } - ) + ); + + // Get the session or user object + const authHeader = req.headers.get('Authorization')!; + const token = authHeader.replace('Bearer ', ''); + const { data } = await supabaseClient.auth.getUser(token); }) ``` @@ -68,11 +71,13 @@ Deno.serve(async (req: Request) => { const supabaseClient = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', - { global: { headers: { Authorization: req.headers.get('Authorization')! } } } - ) + ); - // Database queries will have RLS policies enforced - const { data, error } = await supabaseClient.from('profiles').select('*') + // Get the session or user object + const authHeader = req.headers.get('Authorization')!; + const token = authHeader.replace('Bearer ', ''); + const { data: userData } = await supabaseClient.auth.getUser(token); + const { data, error } = await supabaseClient.from('profiles').select('*'); return new Response(JSON.stringify({ data }), { headers: { 'Content-Type': 'application/json' }, diff --git a/apps/docs/content/guides/functions/import-maps.mdx b/apps/docs/content/guides/functions/dependencies.mdx similarity index 85% rename from apps/docs/content/guides/functions/import-maps.mdx rename to apps/docs/content/guides/functions/dependencies.mdx index 6fcaf870d68..b9abf701bf6 100644 --- a/apps/docs/content/guides/functions/import-maps.mdx +++ b/apps/docs/content/guides/functions/dependencies.mdx @@ -51,11 +51,11 @@ In the Deno ecosystem, each function should be treated as an independent project - Makes deployments more predictable and maintainable - Allows for different versions of the same dependency across functions -For these reasons, we recommend maintaining separate configuration files (`deno.json`/`deno.jsonc`, `.npmrc`, or `import_map.json`) within each function's directory, even if it means duplicating some configurations. +For these reasons, we recommend maintaining separate configuration files (`deno.json`, `.npmrc`, or `import_map.json`) within each function's directory, even if it means duplicating some configurations. There are two ways to manage your dependencies in Supabase Edge Functions: -### Using deno.jsonc (recommended) +### Using deno.json (recommended) @@ -63,7 +63,7 @@ This feature requires Supabase CLI version 1.215.0 or higher. -Each function should have its own `deno.jsonc` or `deno.json` file to manage dependencies and configure Deno-specific settings. This ensures proper isolation between functions and is the recommended approach for deployment. For a complete list of supported options, see the [official Deno configuration documentation](https://docs.deno.com/runtime/manual/getting_started/configuration_file). +Each function should have its own `deno.json` file to manage dependencies and configure Deno-specific settings. This ensures proper isolation between functions and is the recommended approach for deployment. For a complete list of supported options, see the [official Deno configuration documentation](https://docs.deno.com/runtime/manual/getting_started/configuration_file). ```json supabase/functions/my-function/deno.json { @@ -80,24 +80,24 @@ The recommended file structure for deployment: ├── functions │ ├── function-one │ │ ├── index.ts - │ │ ├─- deno.jsonc # Function-specific Deno configuration + │ │ ├─- deno.json # Function-specific Deno configuration │ │ └── .npmrc # Function-specific npm configuration (if needed) │ └── function-two │ ├── index.ts - │ ├─- deno.jsonc # Function-specific Deno configuration + │ ├─- deno.json # Function-specific Deno configuration │ └── .npmrc # Function-specific npm configuration (if needed) └── config.toml ``` - While it's possible to use a global `deno.json` or `deno.jsonc` in the `/supabase/functions` - directory for local development, this approach is not recommended for deployment. Each function - should maintain its own configuration to ensure proper isolation and dependency management. + While it's possible to use a global `deno.json` in the `/supabase/functions` directory for local + development, this approach is not recommended for deployment. Each function should maintain its + own configuration to ensure proper isolation and dependency management. ### Using import maps (legacy) -Import Maps are a legacy way to manage dependencies, similar to a `package.json` file. While still supported, we recommend using `deno.jsonc`. If both exist, `deno.jsonc` takes precedence. +Import Maps are a legacy way to manage dependencies, similar to a `package.json` file. While still supported, we recommend using `deno.json`. If both exist, `deno.json` takes precedence. Each function should have its own `import_map.json` file for proper isolation: @@ -161,7 +161,7 @@ To use private npm packages, create a `.npmrc` file within your function directo └── functions └── my-function ├── index.ts - ├── deno.jsonc + ├── deno.json └── .npmrc # Function-specific npm configuration ``` diff --git a/apps/docs/content/guides/functions/wasm.mdx b/apps/docs/content/guides/functions/wasm.mdx new file mode 100644 index 00000000000..af0bf32a0f5 --- /dev/null +++ b/apps/docs/content/guides/functions/wasm.mdx @@ -0,0 +1,87 @@ +--- +id: 'function-wasm' +title: 'Using Wasm modules' +description: 'How to use WebAssembly in Edge Functions.' +subtitle: 'How to use WebAssembly in Edge Functions.' +--- + +Edge Functions supports running [WebAssembly (Wasm)](https://developer.mozilla.org/en-US/docs/WebAssembly) modules. WebAssembly is useful if you want to optimize code that's slower to run in JavaScript or require low-level manipulation. + +It also gives you the option to port existing libraries written in other languages to be used with JavaScript. For example, [MagickWasm](https://supabase.com/docs/guides/functions/examples/image-manipulation), which does image manipulation and transforms, is a port of an existing C library to WebAssembly. + +### Writing a Wasm module + +You can use different languages and SDKs to write Wasm modules. For this tutorial, we will write a simple Wasm module in Rust that adds two numbers. + +Follow this [guide on writing Wasm modules in Rust](https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_Wasm) to setup your dev environment. + +Create a new Edge Function called `wasm-add`. + +```bash +supabase functions new wasm-add +``` + +Create a new Cargo project for the Wasm module inside the function's directory: + +```bash +cd supabase/functions/wasm-add +cargo new --lib add-wasm +``` + +Add the following code to `add-wasm/src/lib.rs`. + +<$CodeSample +path="edge-functions/supabase/functions/wasm-modules/add-wasm/src/lib.rs" +lines={[[1, -1]]} +meta="lib.rs" +/> + +Update the `add-wasm/Cargo.toml` to include the `wasm-bindgen` dependency. + +<$CodeSample +path="edge-functions/supabase/functions/wasm-modules/add-wasm/Cargo.toml" +lines={[[1, -1]]} +meta="Cargo.toml" +/> + +After that we can build the package, by running: + +```bash +wasm-pack build --target deno +``` + +This will produce a Wasm binary file inside `add-wasm/pkg` directory. + +### Calling the Wasm module from the Edge Function + +Now let's update the Edge Function to call `add` from the Wasm module. + +<$CodeSample +path="edge-functions/supabase/functions/wasm-modules/index.ts" +lines={[[1, -1]]} +meta="index.ts" +/> + + + Supabase Edge Functions currently use Deno 1.46. From [Deno 2.1, importing Wasm + modules](https://deno.com/blog/v2.1) will require even less boilerplate code. + + +### Bundle and deploy the Edge Function + +Before deploying the Edge Function, we need to ensure it bundles the Wasm module with it. We can do this by defining it in the `static_files` for the function in `superbase/config.toml`. + + + You will need update Supabase CLI to 2.7.0 or higher for the `static_files` support. + + +```toml +[functions.wasm-add] +static_files = [ "./functions/wasm-add/add-wasm/pkg/*.wasm"] +``` + +Deploy the function by running: + +```bash +supabase functions deploy wasm-add +``` diff --git a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx index e2f63ba5bc7..3a818e69f83 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx @@ -8,14 +8,8 @@ hideToc: true - - - - - - - + Boostrap the Hono example app from the Supabase Samples using the CLI. @@ -31,7 +25,7 @@ hideToc: true - + The `package.json` file in the project includes the necessary dependencies, including `@supabase/supabase-js` and `@supabase/ssr` to help with server-side auth. @@ -48,7 +42,7 @@ hideToc: true - + Copy the `.env.example` file to `.env` and update the values with your Supabase project URL and anon key. @@ -70,11 +64,13 @@ hideToc: true - + Start the app, go to http://localhost:5173. + Learn how [server side auth](/docs/guides/auth/server-side/creating-a-client?queryGroups=framework&framework=hono) works with Hono. + diff --git a/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx b/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx index 3e0f3fd6875..011d956bac5 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx @@ -63,7 +63,7 @@ hideToc: true ```bash .env DB_CONNECTION=pgsql - DATABASE_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres + DB_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres ``` @@ -84,13 +84,13 @@ hideToc: true ```php app/config/database.php 'pgsql' => [ 'driver' => 'pgsql', - 'url' => env('DATABASE_URL'), + 'url' => env('DB_URL'), 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '5432'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', + 'charset' => env('DB_CHARSET', 'utf8'), 'prefix' => '', 'prefix_indexes' => true, 'search_path' => 'laravel', diff --git a/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx b/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx index 7075b0a1e84..8f78c5c94bb 100644 --- a/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx +++ b/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx @@ -17,23 +17,23 @@ Scopes are set when you [create an OAuth app](/docs/guides/platform/oauth-apps/b ## Available scopes -| Name | Type | Description | -| ---------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `Auth` | `Read` | Retrieve a project's auth configuration
Retrieve a project's SAML SSO providers | -| `Auth` | `Write` | Update a project's auth configuration
Create, update, or delete a project's SAML SSO providers | -| `Database` | `Read` | Retrieve the database configuration
Retrieve SQL snippets
Check if the database is in read-only mode
Retrieve a database's SSL enforcement configuration
Retrieve a database's schema typescript types | -| `Database` | `Write` | Create a SQL query
Enable database webhooks on the project
Update the project's database configuration
Update a database's SSL enforcement configuration
Disable read-only mode for 15mins
Retrieve the pooler configuration
Create a PITR backup for a database | -| `Domains` | `Read` | Retrieve the custom domains for a project
Retrieve the vanity subdomain configuration for a project | -| `Domains` | `Write` | Activate, initialize, reverify, or delete the custom domain for a project
Activate, delete or check the availability of a vanity subdomain for a project | -| `Edge Functions` | `Read` | Retrieve information about a project's edge functions | -| `Edge Functions` | `Write` | Create, update, or delete an edge function | -| `Environment` | `Read` | Retrieve branches in a project | -| `Environment` | `Write` | Create, update, or delete a branch | -| `Organizations` | `Read` | Retrieve an organization's metadata
Retrieve all members in an organization | -| `Organizations` | `Write` | Create an organization | -| `Projects` | `Read` | Retrieve a project's metadata
Check if a project's database is eligible for upgrade
Retrieve a project's network restrictions
Retrieve a project's network bans | -| `Projects` | `Write` | Create a project
Upgrade a project's database
Remove a project's network bans
Update a project's network restrictions | -| `Rest` | `Read` | Retrieve a project's PostgREST configuration | -| `Rest` | `Write` | Update a project's PostgREST configuration | -| `Secrets` | `Read` | Retrieve a project's API keys
Retrieve a project's secrets
Retrieve a project's pgsodium config | -| `Secrets` | `Write` | Create or update a project's secrets
Update a project's pgsodium configuration | +| Name | Type | Description | +| ---------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Auth` | `Read` | Retrieve a project's auth configuration
Retrieve a project's SAML SSO providers | +| `Auth` | `Write` | Update a project's auth configuration
Create, update, or delete a project's SAML SSO providers | +| `Database` | `Read` | Retrieve the database configuration
Retrieve the pooler configuration
Retrieve SQL snippets
Check if the database is in read-only mode
Retrieve a database's SSL enforcement configuration
Retrieve a database's schema typescript types | +| `Database` | `Write` | Create a SQL query
Enable database webhooks on the project
Update the project's database configuration
Update the pooler configuration
Update a database's SSL enforcement configuration
Disable read-only mode for 15mins
Create a PITR backup for a database | +| `Domains` | `Read` | Retrieve the custom domains for a project
Retrieve the vanity subdomain configuration for a project | +| `Domains` | `Write` | Activate, initialize, reverify, or delete the custom domain for a project
Activate, delete or check the availability of a vanity subdomain for a project | +| `Edge Functions` | `Read` | Retrieve information about a project's edge functions | +| `Edge Functions` | `Write` | Create, update, or delete an edge function | +| `Environment` | `Read` | Retrieve branches in a project | +| `Environment` | `Write` | Create, update, or delete a branch | +| `Organizations` | `Read` | Retrieve an organization's metadata
Retrieve all members in an organization | +| `Organizations` | `Write` | Create an organization | +| `Projects` | `Read` | Retrieve a project's metadata
Check if a project's database is eligible for upgrade
Retrieve a project's network restrictions
Retrieve a project's network bans | +| `Projects` | `Write` | Create a project
Upgrade a project's database
Remove a project's network bans
Update a project's network restrictions | +| `Rest` | `Read` | Retrieve a project's PostgREST configuration | +| `Rest` | `Write` | Update a project's PostgREST configuration | +| `Secrets` | `Read` | Retrieve a project's API keys
Retrieve a project's secrets
Retrieve a project's pgsodium config | +| `Secrets` | `Write` | Create or update a project's secrets
Update a project's pgsodium configuration | diff --git a/apps/docs/content/guides/platform/access-control.mdx b/apps/docs/content/guides/platform/access-control.mdx index e0ed0f6a5e1..2b31ca2b5c7 100644 --- a/apps/docs/content/guides/platform/access-control.mdx +++ b/apps/docs/content/guides/platform/access-control.mdx @@ -59,6 +59,7 @@ The table below shows the actions each role can take on the resources belonging | **Organization** | | | | | | | Organization Management | Update | | | | | | | Delete | | | | | +| OpenAI Telemetry Configuration[^2] | Update | | | | | | **Members** | | | | | | | Organization Members | List | | | | | | Owner | Add | | | | | @@ -75,7 +76,7 @@ The table below shows the actions each role can take on the resources belonging | | Remove | | | | | | Invite | Revoke | | | | | | | Resend | | | | | -| | Accept[^2] | | | | | +| | Accept[^3] | | | | | | **Billing** | | | | | | | Invoices | List | | | | | | Billing Email | View | | | | | @@ -115,7 +116,7 @@ The table below shows the actions each role can take on the resources belonging The table below shows the actions each role can take on the resources belonging to the project. -| Resource | Action | Owner | Admin | Developer | Read-Only[^3][^5] | +| Resource | Action | Owner | Admin | Developer | Read-Only[^4][^6] | | ------------------------------------------------------------------------------------------------------ | ---------------------- | :-------------------------------------: | :-------------------------------------: | :-------------------------------------: | :-------------------------------------------------------------: | | **Project** | | | | | | | Project Management | Transfer | | | | | @@ -179,7 +180,7 @@ The table below shows the actions each role can take on the resources belonging | | Create | | | | | | | Delete | | | | | | **Edge Functions Configuration** | | | | | | -| Secrets | View | | | | [^4] | +| Secrets | View | | | | [^5] | | | Create | | | | | | | Delete | | | | | | **SQL Editor** | | | | | | @@ -188,7 +189,7 @@ The table below shows the actions each role can take on the resources belonging | | Delete | | | | | | | View | | | | | | | List | | | | | -| | Run | | | | [^6] | +| | Run | | | | [^7] | | **Database** | | | | | | | Scheduled Backups | View | | | | | | | Download | | | | | @@ -258,8 +259,9 @@ The table below shows the actions each role can take on the resources belonging | | List | | | | | [^1]: Available on the Team and Enterprise Plans. -[^2]: Invites sent from a SSO account can only be accepted by another SSO account coming from the same identity provider. This is a security measure that prevents accidental invites to accounts not managed by your company's enterprise systems. -[^3]: Available on the Enterprise Plan. -[^4]: Read-Only role is able to access secrets. -[^5]: Listed permissions are for the API and Dashboard. -[^6]: Limited to executing SELECT queries. SQL Query Snippets run by the Read-Only role are run against the database using the **supabase_read_only_user**. This role has the [predefined Postgres role pg_read_all_data](https://www.postgresql.org/docs/current/predefined-roles.html). +[^2]: Sending anonymous data to OpenAI is opt in and can improve Studio AI Assistant's responses. +[^3]: Invites sent from a SSO account can only be accepted by another SSO account coming from the same identity provider. This is a security measure that prevents accidental invites to accounts not managed by your company's enterprise systems. +[^4]: Available on the Enterprise Plan. +[^5]: Read-Only role is able to access secrets. +[^6]: Listed permissions are for the API and Dashboard. +[^7]: Limited to executing SELECT queries. SQL Query Snippets run by the Read-Only role are run against the database using the **supabase_read_only_user**. This role has the [predefined Postgres role pg_read_all_data](https://www.postgresql.org/docs/current/predefined-roles.html). diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx index f1bdf94d83e..79a3a4358a8 100644 --- a/apps/docs/content/guides/platform/billing-faq.mdx +++ b/apps/docs/content/guides/platform/billing-faq.mdx @@ -160,13 +160,17 @@ Visa, Mastercard, American Express, Japan Credit Bureau (JCB), China UnionPay (C #### What currency can I pay in? -All our invoices are issued in USD, but you can pay in any currency as long as your credit card provider allows charging in USD after conversion. +All our invoices are issued in USD, but you can pay in any currency so long as the credit card provider allows charging in USD after conversion. #### Can I change the payment method? Yes, you will have to add the new payment method before being allowed to remove the old one. This can be done from your dashboard on the [organization’s billing page](https://supabase.com/dashboard/org/_/billing). +#### Can I pay upfront for multilpe months? + +You can top up your credit balance to cover multiple months through your [organization’s billing page](https://supabase.com/dashboard/org/_/billing). + #### When are payments taken? Payments are taken at the beginning of each billing cycle. You will be charged once a month. You can see the current billing cycle and upcoming invoice in your [organization's billing settings](https://supabase.com/dashboard/org/_/billing). The subscription plan, custom domain and PITR addons are charged upfront, whereas usage-charges, including compute, are charged in hindsight based on your usage. diff --git a/apps/docs/content/guides/platform/org-based-billing.mdx b/apps/docs/content/guides/platform/org-based-billing.mdx index ef394ebe867..90356117a5d 100644 --- a/apps/docs/content/guides/platform/org-based-billing.mdx +++ b/apps/docs/content/guides/platform/org-based-billing.mdx @@ -155,9 +155,9 @@ You can see a breakdown of the different types of egress on your [organization u ## Disk size -We differentiate between database space usage and disk size. Database Space is the actual amount of space used by all your database objects, whereas disk size is the size of the underlying provisioned disk. Each database has a provisioned disk. +We differentiate between database size and disk size. Database size is the actual amount of space used by all your database objects, whereas disk size is the size of the underlying provisioned disk. Each database has a provisioned disk. -Free Plan customers are limited to 500 MB of database space. +Free Plan customers are limited to 500 MB of database size per project. For paid plan customers: @@ -189,7 +189,7 @@ Here are the quotas for the Free Plan: | Usage Item | Free Plan | | ------------------------- | ----------------------------- | | Egress | 5GB across Database + Storage | -| Database Space | 500MB | +| Database Size | 500MB | | Storage Space | 1GB | | Monthly Active Users | 50,000 MAU | | Edge Function Invocations | 500K | diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index 36fb163e15e..f7576f1398b 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -107,17 +107,9 @@ If you upgrade to a paid plan while your project is paused, any expired one-clic [Migrating within Supabase guide](/docs/guides/platform/migrating-within-supabase)
-If the 90 day project restore window has expired but you need to access data contained within your project using SQL, you can attempt to restore the project into a local Postgres instance. Supabase publishes tooling that can be used for that purpose. Be aware that this workflow does not produce a complete Supabase environment with REST/Auth/Storage. Instead, it creates a standalone Postgres instance that is maximally compatible with your project's backup file to assist with recovering your data. +If the 90 day project restore window has expired but you need to access data contained within your project using SQL, you can use the [Supabase CLI](/docs/guides/local-development) to restore the project into a local Postgres instance. -Downloaded backups are plain text SQL files that can be executed in Postgres to restore your project's schema and data. If you need to access data within a backup we recommend restoring into a Postgres instance built using the Supabase open source build tooling to ensure that the local instance is as similar to the original project as possible. Here are the steps to start Supabase Postgres locally and restore the backup: - -Install [nix](https://nixos.org/): - -```sh -curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install -``` - -To identify your project's backup image version, reference the backup's download button following the `PG:` prefix: +First, download your project's backup file from dashboard and identify its backup image version (following the `PG:` prefix): -Given Postgres version `15.6.1.115`, start Postgres locally with the nix command: +Given Postgres version `15.6.1.115`, start Postgres locally with `db_cluster.backup` being the path to your backup file. ```sh -nix run 'github:supabase/postgres?ref=15.6.1.115#start-server' 15 +supabase init +echo '15.6.1.115' > supabase/.temp/postgres-version +supabase db start --from-backup db_cluster.backup ``` -where `15` refers to the backup's Postgres major version. +Note that the earliest Supabase Postgres version that supports a local restore is `15.1.0.55`. If your hosted project was running on earlier versions, you will likely run into errors during restore. Before submitting any support ticket, make sure you have attached the error logs from `supabase_db_*` docker container. -Note that the earliest Supabase Postgres version that supports a direct reference is `15.6.1.115`. If you don't know your project's Postgres version or it is lower than `15.6.1.115` then omit the `ref` option to build our most recent image: +Once your local database starts up successfully, you can connect using psql to verify that all your data is restored. ```sh -nix run 'github:supabase/postgres#start-server' -- 15 +psql 'postgresql://postgres:postgres@localhost:54322/postgres' ``` -Then restore the backup file from a separate shell session: +If you want to use other services like Auth, Storage, and Studio dashboard together with your restored database, restart the local development stack. ```sh -nix run github:supabase/postgres#start-client -- --file +supabase stop +supabase start ``` -Which loads the backup into Postgres and drops you into a [psql](https://www.postgresql.org/docs/current/app-psql.html) prompt. For `start-client` you do not need to provide a reference to the backup's Postgres version. - -Once your project has been restored, connect to it with your preferred Postgres tooling using `host=localhost`, `user=postgres`, `port=5432`, and an empty password. This will allow you to inspect your saved data and export whatever is necessary. - -Errors encountered during the restore process are logged to the console by the `start-server` process. Review those errors and edit the backup file as needed until the project restores successfully. The backup file is a plain text sequence of SQL statements that recreate your database. To resolve common error classes like `relation "xxx" does not exist`, identify the line raising the exception and shuffle the `create ... "xxx"` statement before it. A failed restore leaves artifacts on the Postgres server so be sure to restart the `start-server` process between restore attempts. - -A Postgres database started with `start-server` command is not production ready and should never be used outside a local development workflow. +A Postgres database started with Supabase CLI is not production ready and should not be used outside of local development. #### Disk sizing diff --git a/apps/docs/content/guides/realtime/broadcast.mdx b/apps/docs/content/guides/realtime/broadcast.mdx index 09291a4e6ea..57c94f8ab4a 100644 --- a/apps/docs/content/guides/realtime/broadcast.mdx +++ b/apps/docs/content/guides/realtime/broadcast.mdx @@ -224,6 +224,35 @@ You can provide a callback for the `broadcast` channel to receive message. This }) ``` + + + + We can send Broadcast messages using `channelB.send()`. Let's set up another client to send messages. + + We can wrap the subscription in a promise to use async/await. + + {/* prettier-ignore */} + ```js + // Join a room/topic. Can be anything except for 'realtime'. + const channelB = supabase.channel('room-1') + + const channelSubscribed = await new Promise((resolve, reject) => { + channelB.subscribe((status) => { + if (status === 'SUBSCRIBED') { + resolve(channel); + } else { + reject("Could not subscribe to channel"); + } + }) + }); + + channelSubscribed.send({ + type: 'broadcast', + event: 'test', + payload: { message: 'hello, world' }, + }); + ``` + diff --git a/apps/docs/content/guides/storage/management/delete-objects.mdx b/apps/docs/content/guides/storage/management/delete-objects.mdx index 986eb8661bf..8f5fc34b81a 100644 --- a/apps/docs/content/guides/storage/management/delete-objects.mdx +++ b/apps/docs/content/guides/storage/management/delete-objects.mdx @@ -32,6 +32,6 @@ on storage.objects for delete TO authenticated USING ( - owner = (select auth.uid()) + owner = (select auth.uid()::text) ); ``` diff --git a/apps/docs/content/troubleshooting/42501--permission-denied-for-table-httprequestqueue-KnozmQ.mdx b/apps/docs/content/troubleshooting/42501--permission-denied-for-table-httprequestqueue-KnozmQ.mdx new file mode 100644 index 00000000000..efd2ad6e358 --- /dev/null +++ b/apps/docs/content/troubleshooting/42501--permission-denied-for-table-httprequestqueue-KnozmQ.mdx @@ -0,0 +1,18 @@ +--- +title = "42501 : permission denied for table http_request_queue" +github_url = "https://github.com/orgs/supabase/discussions/21450" +date_created = "2024-02-22T10:26:29+00:00" +topics = [ "database" ] +keywords = [ "permission", "pg_net", "http_request_queue" ] +database_id = "97923126-7d0b-4bf0-bb65-55dae289a8a3" + +[[errors]] +code = "42501" +message = "permission denied for table http_request_queue" +--- + +If you're currently blocked by the above error, please run the following in your Supabase SQL editor: + +- Check `select * from net.http_request_queue` and make sure it's empty. +- Try `drop extension pg_net; create extension pg_net schema extensions;` +- If that doesn't work (e.g. because some objects depend on it), then please [contact support](https://supabase.com/support). diff --git a/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx new file mode 100644 index 00000000000..df650ef1225 --- /dev/null +++ b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx @@ -0,0 +1,34 @@ +--- +title = "All about Supabase Egress" +github_url = "https://github.com/orgs/supabase/discussions/26605" +date_created = "2024-05-21T11:52:49+00:00" +topics = [ "platform", "database", "functions", "storage", "realtime", "auth", "supavisor" ] +keywords = [ "egress", "bandwidth" ] +database_id = "ba593989-12b5-464f-8ede-a525e1ca2ffb" +--- + +**What is Egress?** + +Egress (also known as bandwidth) is any amount of network packets/bytes being streamed back to a connected client. Means, the data that is leaving the Supabase platform. Egress in Supabase includes any calls through PostgREST, to Storage, Realtime, Auth, Edge Functions, Database and Supavisor. + +You can read about Unified egress, included quota, and how to check the egress usage here: https://supabase.com/docs/guides/platform/org-based-billing#unified-egress. Additionally, the [project reports](https://supabase.com/dashboard/project/_/reports) have a few egress related stats. You can create a custom report to look into daily egress. + +- Example: Log Explorer -> Custom Reports -> Add/Remove charts -> Database API -> API Egress. + +**What is contributing to Egress?** + +While pointing out the exact cause for egress may not be straightforward, there are various steps you can take to determine the source of these issues: + +- Picking the "Top Paths" from the [log explorer](https://app.supabase.com/project/_/logs/explorer/templates) will help you identify heavily queried paths +- By finding the most requested queries from Query performance report: https://app.supabase.com/project/_/reports/query-performance +- Supavisor Egress is independent of client. There is no direct relation between a single query and Supavisor egress, it is harder to debug and identify. But you can make use of frequent queries from the link in above step that also displays average number of rows which will help to identify queries with a large number of rows returned. While this does not display Supavisor queries specifically, it will give an overview of queries with lots of rows that can help. +- For Storage Egress, all outgoing traffic for storage-related requests to download/view your Storage items are considered as Storage egress. We have a "Storage Egress Requests" template in logs explorer that you can use to get the number of requests for each Storage object +- If you pull 1mb of data out of the database using the Supavisor connection in your edge function, but only sends 100kb back to the user, you will have the Egress from the Supavisor to your Edge function plus from the edge function to your user + +**How can you decrease egress?** + +- Reduce the number of fields selected or entries when querying +- Reduce the number of queries/calls by optimising client code or use caches to reduce the number of requests/queries being done: https://github.com/psteinroe/supabase-cache-helpers/ +- In case of update/insert queries, if you don’t need the entire row to be returned, configure your ORM/queries to not return the entire row +- In case of running manual backups through Supavisor, remove unneeded tables and/or reduce the frequency +- For Storage, if you start using the [smartCDN](https://supabase.com/docs/guides/storage/cdn/smart-cdn) Storage Egress usage can be managed. You can also use the [Supabase Image Transformations](https://supabase.com/docs/guides/storage/image-transformations) to optimize the images and reduce the egress. diff --git a/apps/docs/content/troubleshooting/an-invalid-response-was-received-from-the-upstream-server-error-when-querying-auth-RI4Vl-.mdx b/apps/docs/content/troubleshooting/an-invalid-response-was-received-from-the-upstream-server-error-when-querying-auth-RI4Vl-.mdx new file mode 100644 index 00000000000..6f227758155 --- /dev/null +++ b/apps/docs/content/troubleshooting/an-invalid-response-was-received-from-the-upstream-server-error-when-querying-auth-RI4Vl-.mdx @@ -0,0 +1,30 @@ +--- +title = "An \"invalid response was received from the upstream server\" error when querying auth" +github_url = "https://github.com/orgs/supabase/discussions/20722" +date_created = "2024-01-25T07:08:43+00:00" +topics = [ "auth" ] +database_id = "7183351e-dba3-484d-83b3-bb89a027e36a" + +[[errors]] +message = "invalid response was received from the upstream server" + +[[errors]] +message = "running db migrations: error executing migrations/20221208132122_backfill_email_last_sign_in_at.up.sql" + +[[errors]] +message = "operator does not exist: uuid = text" +--- + +If you are observing an "invalid response was received from the upstream server" error when making requests to Supabase Auth, it could mean that the respective service is down. One way to confirm this is to visit the [logs explorer](https://supabase.com/dashboard/project/_/logs/explorer) and look at the auth logs to see if there are any errors with the following lines: + +- `running db migrations: error executing migrations/20221208132122_backfill_email_last_sign_in_at.up.sql` + +We're currently investigating an issue where the tables responsible for keeping track of migrations ran by Auth (`auth.schema_migrations`) are not being restored properly, which leads to the service(s) retrying those migrations. In such cases, migrations which are not idempotent will run into issues. + +We've documented some of the migrations that run into this issue and their corresponding fix here: + +### Auth: `operator does not exist: uuid = text` + +Temporary fix: Run `insert into auth.schema_migrations values ('20221208132122');` via the [sql editor](https://supabase.com/dashboard/project/_/sql/new) to fix the issue. + +If the migration error you're seeing looks different, please reach out to [supabase.help](https://supabase.help/) for assistance. diff --git a/apps/docs/content/troubleshooting/are-all-features-available-in-self-hosted-supabase-THPcqw.mdx b/apps/docs/content/troubleshooting/are-all-features-available-in-self-hosted-supabase-THPcqw.mdx new file mode 100644 index 00000000000..350e43f8b5c --- /dev/null +++ b/apps/docs/content/troubleshooting/are-all-features-available-in-self-hosted-supabase-THPcqw.mdx @@ -0,0 +1,21 @@ +--- +title = "Are all features available in self-hosted Supabase?" +github_url = "https://github.com/orgs/supabase/discussions/27299" +date_created = "2024-06-16T09:33:39+00:00" +topics = [ "self-hosting" ] +database_id = "03854567-8838-4f12-8a6c-095fc1671d9f" +--- + +### Overview + +The self-hosted version is pretty similar to the hosted one. It might not always have the latest features right away, but it includes everything you need to get your application up and running. + +### Feature availability + +To know what features are available in the self-hosted version, refer to the comprehensive list here: +[Supabase Features](https://supabase.com/docs/guides/getting-started/features#generally-available) + +### Self-hosting documentation + +For detailed steps and guidance on how to set up and manage your self-hosted Supabase instance, follow the documentation provided: +[Self-Hosting Guide](https://supabase.com/docs/guides/self-hosting) diff --git a/apps/docs/content/troubleshooting/auth-error-401-invalid-claim-missing-sub--AFwMR.mdx b/apps/docs/content/troubleshooting/auth-error-401-invalid-claim-missing-sub--AFwMR.mdx new file mode 100644 index 00000000000..3652511e74a --- /dev/null +++ b/apps/docs/content/troubleshooting/auth-error-401-invalid-claim-missing-sub--AFwMR.mdx @@ -0,0 +1,31 @@ +--- +title = "Auth error: {401: invalid claim: missing sub}" +github_url = "https://github.com/orgs/supabase/discussions/21347" +date_created = "2024-02-18T13:47:37+00:00" +topics = [ "auth" ] +keywords = [ "JWT", "Server-Side Rendering" ] +database_id = "e90ca9e0-0376-4313-b32b-b9a4ae81bf7a" + +[api] +sdk = [ "auth-getuser" ] + +[[errors]] +http_status_code = 401 +message = "invalid claim: missing sub" +--- + +The missing sub claim error is returned when supabase.auth.getUser() is called with an invalid JWT in the session or when the user attempts to register/sign in but hasn't completed the sign in when the getUser call is made. + +A common pitfall, is inadvertently using a Supabase API key (such as the anon or service_role keys) instead of the Supabase Auth access token. + +**Why Does This Happen?** + +- The Supabase API keys are designed for different purposes and, while they are recognized by the Supabase Auth system, they do not carry the sub claim. The sub claim is essential as it encodes the user ID, which is a mandatory field for authentication processes. This mistake leads to the "missing sub claim" error because the system expects a token that contains user identification information. + +**How to Avoid This Issue:** + +- Ensure that the token being passed to supabase.auth.getUser() is, indeed, an Auth access token and not one of the API keys. + +- Are you creating the client on a per-request basis or are you creating a global client to be shared? If you're creating the client on a per-request basis, then you need to pass the session with the user's JWT from the client to the server somehow. This can be done by sending the user's JWT in a header like an `Authorization: Bearer ` . You can then get this header and call supabase.auth.getUser(user_jwt) with the user's JWT. + +- Examine how the Supabase client is being initialized, especially in server-side scenarios. diff --git a/apps/docs/content/troubleshooting/avoiding-timeouts-in-long-running-queries-6nmbdN.mdx b/apps/docs/content/troubleshooting/avoiding-timeouts-in-long-running-queries-6nmbdN.mdx new file mode 100644 index 00000000000..82f46abac2e --- /dev/null +++ b/apps/docs/content/troubleshooting/avoiding-timeouts-in-long-running-queries-6nmbdN.mdx @@ -0,0 +1,62 @@ +--- +title = "Avoiding timeouts in long running queries" +github_url = "https://github.com/orgs/supabase/discussions/21133" +date_created = "2024-02-09T02:46:42+00:00" +topics = [ "database", "platform" ] +keywords = [ "timeout", "query", "database", "psql", "compute" ] +database_id = "8bb130d5-d298-49c7-8cfb-dd7a3f231154" +--- + + + +This page covers long-running queries via the Dashboard or external interfaces. For Supabase Client API timeout errors, check this [guide](https://github.com/orgs/supabase/discussions/14256) instead. + + + +Certain queries, like indexing a table or changing a column's data type, are inherently time-consuming. The [Dashboard's SQL Editor](https://supabase.com/dashboard/project/_/sql/) has a 1-minute timeout limit. + +To execute long-running queries, follow the below steps. + +### Install an external SQL client + +The guide focuses on [psql](https://supabase.com/docs/guides/database/psql) but you can use any Postgres client. + +Some other options include: + +- [pgadmin](https://supabase.com/docs/guides/database/pgadmin) +- [DBeaver](https://supabase.com/docs/guides/database/dbeaver) + +You can install PSQL in [macOS](https://stackoverflow.com/a/49689589/2188186) and [Windows](https://www.postgresql.org/download/windows/) by following these links and instructions. For Linux (Debian) you can run the following: + +```bash +sudo apt-get update +sudo apt-get install postgresql-client +``` + +Once installed, you can find your PSQL string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database). Please make sure if you are using the pooler connection that it is in session mode (port 5432). + + + +If you are working in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have the IPv4 Add-On, it is preferable to use the direct connection. + + + +### Increase the query timeout + +Then you can increase the query timeout solely for your session: + +Long timeout + +```sql +set statement_timeout = '120min'; +``` + +Disable timeout + +```sql +set statement_timeout = '0'; +``` + +If your task is particularly long, you can may want to consider boosting your computing power temporarily. Compute size on Supabase is charged by the hour, so you can increase it for an hour or two, complete your task faster, then scale it back afterward. + +If you want to temporarily upgrade, you can find the add-ons for your project in your [Dashboard's Add-Ons Settings.](https://supabase.green/dashboard/project/_/settings/addons) diff --git a/apps/docs/content/troubleshooting/canceling-statement-due-to-statement-timeout-581wFv.mdx b/apps/docs/content/troubleshooting/canceling-statement-due-to-statement-timeout-581wFv.mdx new file mode 100644 index 00000000000..5fcc1973d8c --- /dev/null +++ b/apps/docs/content/troubleshooting/canceling-statement-due-to-statement-timeout-581wFv.mdx @@ -0,0 +1,18 @@ +--- +title = "Canceling statement due to \"statement timeout\"" +github_url = "https://github.com/orgs/supabase/discussions/14256" +date_created = "2023-05-10T08:29:05+00:00" +topics = [ "database" ] +keywords = [ "statement", "timeout", "postgres", "performance" ] +database_id = "4a41e734-6883-452f-bc8e-82ff4ac22a81" +--- + +> If encountering 504 or timeout errors in the Dashboard, check out this [guide](https://github.com/orgs/supabase/discussions/21133#discussioncomment-9573776) + +You can run this query to check the current settings set for your roles: `SELECT rolname, rolconfig FROM   pg_roles;` + +To increase the `statement_timeout` for a specific role, you may follow the instructions [here](https://supabase.com/docs/guides/database/timeouts#changing-the-default-timeout). Note that it may require a quick reboot for the changes to take effect. + +Additionally, to check how long a query is taking, you can check the Query Performance report which can give you more information on the query's performance: https://app.supabase.com/project/_/reports/query-performance. You can use the [query plan analyzer](https://www.postgresql.org/docs/current/sql-explain.html) on any expensive queries that you have identified: `explain analyze ;`. For supabase-js/ postgREST queries you can use `.explain()`. + +You can also make use of Postgres logs that will give you useful information like when the query was executed: https://app.supabase.com/project/_/logs/postgres-logs. diff --git a/apps/docs/content/troubleshooting/certain-operations-are-too-complex-to-perform-directly-using-the-client-libraries-8JaphH.mdx b/apps/docs/content/troubleshooting/certain-operations-are-too-complex-to-perform-directly-using-the-client-libraries-8JaphH.mdx new file mode 100644 index 00000000000..5fca253ecea --- /dev/null +++ b/apps/docs/content/troubleshooting/certain-operations-are-too-complex-to-perform-directly-using-the-client-libraries-8JaphH.mdx @@ -0,0 +1,56 @@ +--- +title = "Certain operations are too complex to perform directly using the client libraries." +github_url = "https://github.com/orgs/supabase/discussions/21294" +date_created = "2024-02-15T12:37:11+00:00" +topics = [ "database" ] +keywords = [ "stored", "function", "complex" ] +database_id = "df9518df-d019-42bb-8d91-918c6d57b452" +--- + +**Solution** +In cases where operations are overly complex or not feasible to implement directly using the client libraries, it might be beneficial to leverage stored functions within your database. + +Follow these steps to create and run a stored function: + +**Create the Stored Function:** + +Go to the [SQL query editor](https://supabase.com/dashboard/project/_/sql/new) on your database dashboard. +Run the following SQL script to create a stored function tailored to your specific complex query: + +``` +DROP FUNCTION IF EXISTS get_my_complex_query; +CREATE FUNCTION get_my_complex_query(parameter INT) +RETURNS TABLE (column1 INTEGER, column2 VARCHAR, column3 DATE) AS +$$ +BEGIN + RETURN QUERY + SELECT t1.column1, t1.column2, t2.column3 + FROM "TableName1" AS t1 + INNER JOIN "TableName2" AS t2 ON t1.column = t2.column + INNER JOIN "TableName3" AS t3 ON t2.another_column = t3.another_column + LEFT JOIN "TableName4" AS t4 ON t3.some_column = t4.some_column + WHERE t2.column = parameter + AND t3.column_name = 'some_value'; +END; +$$ +LANGUAGE plpgsql VOLATILE; +``` + +**Call the Stored Function:** + +Use the supabase.rpc method to call the stored function from your application code. Replace "get_my_complex_query" with the appropriate function name and provide the necessary parameters: + +``` +supabase.rpc("get_my_complex_query", { parameter: 1 }) + .then(response => { + // Handle the response + }) + .catch(error => { + // Handle errors + }); +``` + +**Further Resources:** + +For more information on PostgreSQL database functions, refer to the following resource: +[Supabase Stored Procedures](https://supabase.com/docs/guides/database/functions#quick-demo) diff --git a/apps/docs/content/troubleshooting/change-email-associated-with-supabase-account-T5eHNT.mdx b/apps/docs/content/troubleshooting/change-email-associated-with-supabase-account-T5eHNT.mdx new file mode 100644 index 00000000000..34ae4d9882a --- /dev/null +++ b/apps/docs/content/troubleshooting/change-email-associated-with-supabase-account-T5eHNT.mdx @@ -0,0 +1,23 @@ +--- +title = "Change email associated with Supabase account" +github_url = "https://github.com/orgs/supabase/discussions/21147" +date_created = "2024-02-09T11:58:06+00:00" +topics = [ "platform", "auth" ] +keywords = [ "email", "account", "authentication" ] +database_id = "6de3c3a9-8614-48d7-827e-cf41d6a1ce23" +--- + +Currently, there isn't a direct way to change the email address of an existing Supabase account. + +If you are using Email Authentication to login into your Supabase account: + +- Invite a new email address to your Supabase Organization(s) as an Organization owner. +- After signing up for a new account with your new email address and confirming you have access to the Organization(s) with your new account, you can leave the Organization(s) from your previous account. + +If you are using Github Authentication to login into your Supabase account: + +- Log out of Supabase. +- Change Primary Email in GitHub +- Log out of Github. +- Log back into Github (with the new, desired email set as primary) +- Log back into Supabase. diff --git a/apps/docs/content/troubleshooting/change-project-region-eWJo5Z.mdx b/apps/docs/content/troubleshooting/change-project-region-eWJo5Z.mdx new file mode 100644 index 00000000000..62d690bc448 --- /dev/null +++ b/apps/docs/content/troubleshooting/change-project-region-eWJo5Z.mdx @@ -0,0 +1,12 @@ +--- +title = "Change Project Region" +github_url = "https://github.com/orgs/supabase/discussions/16341" +date_created = "2023-08-08T14:44:20+00:00" +topics = [ "platform", "database", "auth" ] +keywords = [ "region", "migration" ] +database_id = "fd711d33-9703-402f-84b2-8e74034e7690" +--- + +The process to change the region of a Supabase Project is to create a new project in the desired region and migrate your existing project to the new project using this guide: https://supabase.com/docs/guides/database#migrating-between-projects + +If you are using third-party auth(Facebook, Google, etc.), you need to manually copy over your client id/secret pairs in the dashboard. Also, you'd need to change your API URL/Anon or Service Keys, which is usually done via ENV vars on your web host. diff --git a/apps/docs/content/troubleshooting/check-usage-for-monthly-active-users-mau-MwZaBs.mdx b/apps/docs/content/troubleshooting/check-usage-for-monthly-active-users-mau-MwZaBs.mdx new file mode 100644 index 00000000000..68559d3b4eb --- /dev/null +++ b/apps/docs/content/troubleshooting/check-usage-for-monthly-active-users-mau-MwZaBs.mdx @@ -0,0 +1,16 @@ +--- +title = "Check usage for monthly active users (MAU)" +github_url = "https://github.com/orgs/supabase/discussions/16965" +date_created = "2023-08-29T12:30:52+00:00" +topics = [ "auth", "platform" ] +keywords = [ "MAU", "Monthly active users" ] +database_id = "1759134d-a98d-4bfa-9501-71e06180700e" +--- + +You can see the usage in your [projects usage page](https://app.supabase.com/project/_/settings/billing/usage). + +For MAU, we rely on the [Auth Server](https://github.com/supabase/auth) logs. MAU count is relative to your billing cycle and resets whenever your billing cycle resets. You can check your Auth logs in your [projects logs & analytics section](https://supabase.com/dashboard/project/_/logs/auth-logs). + +We do a distinct count of all user ids in the billing cycle. An Auth event can be a login, token refresh, logout, ... If an authenticated user does any of this, we count it towards the MAU. A user is only counted once towards MAU in a billing cycle. + +The log retention for you as a user (accessible time) depends on your plan. Free plan users can access the logs of the last day, Pro plan users 7 days, Team plan users 28 days, Enterprise users 90 days. Unless you're on an Enterprise plan, you won't be able to execute the query to determine MAU yourself, as you don't have access to the logs in the past 30ish days (depending on your billing cycle). diff --git a/apps/docs/content/troubleshooting/converted-github-account-to-organisation---lost-supabase-account-access-5wsE_1.mdx b/apps/docs/content/troubleshooting/converted-github-account-to-organisation---lost-supabase-account-access-5wsE_1.mdx new file mode 100644 index 00000000000..838a4806b2d --- /dev/null +++ b/apps/docs/content/troubleshooting/converted-github-account-to-organisation---lost-supabase-account-access-5wsE_1.mdx @@ -0,0 +1,12 @@ +--- +title = "Converted Github account to organization - Lost Supabase account access" +github_url = "https://github.com/orgs/supabase/discussions/21101" +date_created = "2024-02-08T01:39:25+00:00" +topics = [ "auth" ] +keywords = [ "github", "organization", "access" ] +database_id = "94c6e33b-cc05-49b3-b010-7480f8dde843" +--- + +[Converting](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-your-personal-account/converting-a-user-into-an-organization) your GitHub account to a [GitHub organization](https://docs.github.com/en/get-started/learning-about-github/types-of-github-accounts#organization-accounts) will cause you to lose access to all other accounts using GitHub sign-ins. + +However, Supabase can help you transfer ownership of an existing organization on your account by inviting a new owner to your organization. This new user will need to have a different email from the previous one, and you will need to email support@supabase.com with that new email as well to confirm that you own the email address. diff --git a/apps/docs/content/troubleshooting/customizing-emails-by-language-KZ_38Q.mdx b/apps/docs/content/troubleshooting/customizing-emails-by-language-KZ_38Q.mdx new file mode 100644 index 00000000000..5c5f5aaf1bb --- /dev/null +++ b/apps/docs/content/troubleshooting/customizing-emails-by-language-KZ_38Q.mdx @@ -0,0 +1,48 @@ +--- +title = "Customizing Emails by Language" +github_url = "https://github.com/orgs/supabase/discussions/21227" +date_created = "2024-02-13T16:03:27+00:00" +topics = [ "auth" ] +keywords = [ "emails" ] +database_id = "2ebc2849-0cf7-4c94-ac13-22167a196a92" +--- + +When you register a user, you can create meta-data about them. + +Creating meta-data with the JS-Client's [signUp function](https://supabase.com/docs/reference/javascript/auth-signup?example=sign-up-with-additional-user-metadata) + +```javascript +const { data, error } = await supabase.auth.signUp({ + email: 'email@some_[email.com](http://email.com/)', + password: 'example-password', + options: { + data: { + first_name: 'John', + last_name: 'Doe', + age: 27, + }, + }, +}) +``` + +The above example creates a user entry that includes information about their name and age. The data is stored in the auth.users table in the `auth.raw_user_meta_data` column. You can view it in the auth schema with the [SQL Editor](https://supabase.com/dashboard/project/_/editor). + +It can be accessed in a project's [Email Templates](https://supabase.com/dashboard/project/_/auth/templates). Below is an example: + +![image](/docs/img/troubleshooting/3eeb2435-dd1c-41bc-9557-44cabff38f59.png) + +If you need to update a user's meta-data, you can do so with the [updateUser](https://supabase.com/docs/reference/javascript/auth-updateuser?example=update-the-users-metadata) function. + +The meta-data can be used to store a users language preferences. You could then use "if statements" in the email template to set the response for a specific language: + +```html +{{if eq .Data.langauge "en" }} +

Welcome!

+{{ else if eq .Data.langauge "pl" }} +

Witamy!

+{{ else }} +

chuS'ugh, tera' je (Klingon)

+{{end}} +``` + +Supabase uses the [Go Templating Language](https://pkg.go.dev/text/template) to render emails. It has advanced features for conditions that you may want to [explore](https://gohugo.io/templates/introduction/). For more examples, there is a [GitHub discussion](https://github.com/supabase/gotrue/issues/80#issuecomment-1552264148) that discusses advanced language templates. diff --git a/apps/docs/content/troubleshooting/dashboard-errors-when-managing-users-N1ls4A.mdx b/apps/docs/content/troubleshooting/dashboard-errors-when-managing-users-N1ls4A.mdx new file mode 100644 index 00000000000..a10ab6d97d5 --- /dev/null +++ b/apps/docs/content/troubleshooting/dashboard-errors-when-managing-users-N1ls4A.mdx @@ -0,0 +1,105 @@ +--- +title = "Dashboard errors when managing users" +github_url = "https://github.com/orgs/supabase/discussions/21247" +date_created = "2024-02-14T04:26:37+00:00" +topics = [ "auth", "studio" ] +keywords = [ "users" ] +database_id = "c5d8260b-c17a-4f6c-ab20-801f953f6c94" + +[api] +sdk = [ "auth-signup" ] + +[[errors]] +message = "Failed to send magic link: failed to make magiclink request: Error sending magic link" + +[[errors]] +message = "Failed to delete user: Database error deleting user" + +[[errors]] +message = "Failed to create user: Database error creating new user" + +[[errors]] +http_status_code = 500 +message = "Database error saving new user" +--- + +## PROBLEM + +### Receiving the following or _similar_ error messages in the Dashboard when managing users. + +> ... Database error +> ... Error sending + + + + + +### Or, receiving a comparable 500 error from the Auth REST API: + +> Database error ... + +Screenshot 2024-02-13 at 10 59 17 PM + +## SOLUTION 1 (trigger related) + +Check if the auth schema contains any triggers in the [Dashboard's trigger section](https://supabase.com/dashboard/project/_/database/triggers). Remove all triggers by dropping their functions with a CASCADE modifier: + +```sql +DROP FUNCTION () CASCADE; + +-- If you'd prefer, you can drop the trigger alone with the following query: +-- DROP TRIGGER on auth.; +``` + +Then recreate the functions with a [security definer](https://supabase.com/docs/guides/database/functions#security-definer-vs-invoker) modifier before recreating the triggers. + +The [SQL Editor](https://supabase.com/dashboard/project/_/sql/) contains a template for [User Management](https://supabase.com/dashboard/project/_/sql/quickstarts). Within it, there is a working example of how to setup triggers with security definers that may be worth referencing: + +```sql +create table profiles ( + id uuid references auth.users on delete cascade not null primary key, + updated_at timestamp with time zone, + username text unique, + full_name text, + avatar_url text, + website text, + + constraint username_length check (char_length(username) >= 3) +); + +create function public.handle_new_user() +returns trigger as $$ +begin + insert into public.profiles (id, full_name, avatar_url) + values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url'); + return new; +end; +$$ language plpgsql security definer; + +create trigger on_auth_user_created + after insert on auth.users + for each row execute procedure public.handle_new_user(); +``` + +### EXPLANATION + +One of the most common design patterns in Supabase is to add a trigger to the `auth.users` table. The database role managing authentication (supabase_auth_admin) only has the necessary permissions it needs to perform its duties. So, when a trigger operated by the supabase_auth_admin interacts outside the auth schema, it causes a permission error. + +A security definer function retains the privileges of the database user that created it. As long as it is the `postgres` role, your auth triggers should be able to engage with outside tables. + +## SOLUTION 2 (constraint related) + +If you did not create a trigger, check if you created a foreign/primary key relationship between the auth.users table and another table. If you did, then ALTER the [behavior](https://stackoverflow.com/questions/5383612/setting-up-table-relations-what-do-cascade-set-null-and-restrict-do) of the relationship and recreate it with a less [restrictive constraint](https://stackoverflow.com/questions/3359329/how-to-change-the-foreign-key-referential-action-behavior). diff --git a/apps/docs/content/troubleshooting/database-error-remaining-connection-slots-are-reserved-for-non-replication-superuser-connections-3V3nIb.mdx b/apps/docs/content/troubleshooting/database-error-remaining-connection-slots-are-reserved-for-non-replication-superuser-connections-3V3nIb.mdx new file mode 100644 index 00000000000..8e50b248103 --- /dev/null +++ b/apps/docs/content/troubleshooting/database-error-remaining-connection-slots-are-reserved-for-non-replication-superuser-connections-3V3nIb.mdx @@ -0,0 +1,20 @@ +--- +title = "Database Error: remaining connection slots are reserved for non-replication superuser connections" +github_url = "https://github.com/orgs/supabase/discussions/13995" +date_created = "2023-04-27T10:39:24+00:00" +topics = [ "database" ] +keywords = [ "connections", "connection slots", "reserved", "superuser" ] +database_id = "b3a73631-0b8d-460d-8033-28df88d26d75" + +[[errors]] +message = "remaining connection slots are reserved for non-replication superuser connections" +--- + +This error usually occurs when the database reaches the maximum number of connections allowed based on the compute add-on. + +To overcome this, the connections need to be optimized as mentioned here: https://supabase.com/docs/guides/platform/performance#optimizing-the-number-of-connections + +Additionally, you can try using the connection pool to help solve this issue: +https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pooler + +If you're already using connection pooling and still hitting the maximum connections, then it is suggested to upgrade your compute add-on that allows more connections: https://supabase.com/docs/guides/platform/compute-add-ons diff --git a/apps/docs/content/troubleshooting/database-error-saving-new-user-RU_EwB.mdx b/apps/docs/content/troubleshooting/database-error-saving-new-user-RU_EwB.mdx new file mode 100644 index 00000000000..d054f6f4020 --- /dev/null +++ b/apps/docs/content/troubleshooting/database-error-saving-new-user-RU_EwB.mdx @@ -0,0 +1,28 @@ +--- +title = "Database error saving new user" +github_url = "https://github.com/orgs/supabase/discussions/13043" +date_created = "2023-03-16T04:52:50+00:00" +topics = [ "auth", "studio" ] +keywords = [ "users" ] +database_id = "5d1c44ed-b2f6-4509-9312-1eeb8838e701" + +[[errors]] +message = "Database error saving new user" +--- + +You generally get this error when trying to invite a new user from the dashboard or when trying to insert a user into a table using the table editor in the Supabase dashboard. + +This error is normally associated with a side effect of a database transaction. + +**Common causes of this error:** + +- You have a trigger/trigger function setup on the `auth.users` table +- You have added a constraint on the `auth.users` table which isn't being met +- You are using Prisma and it has broken all the permissions on the `auth.users` table + +**Debugging this error:** + +- You can use the [Auth logs explorer](https://app.supabase.com/project/_/logs/auth-logs) to find the issue with more information +- You can use the [Postgres logs explorer](https://app.supabase.com/project/_/logs/postgres-logs) + +https://user-images.githubusercontent.com/79497/225517698-b6e3ccaf-cd70-4acd-8124-ffbcee310d63.mp4 diff --git a/apps/docs/content/troubleshooting/deprecated-rls-features-Pm77Zs.mdx b/apps/docs/content/troubleshooting/deprecated-rls-features-Pm77Zs.mdx new file mode 100644 index 00000000000..b27677b816b --- /dev/null +++ b/apps/docs/content/troubleshooting/deprecated-rls-features-Pm77Zs.mdx @@ -0,0 +1,45 @@ +--- +title = "Deprecated RLS features" +github_url = "https://github.com/orgs/supabase/discussions/16703" +date_created = "2023-08-22T13:17:50+00:00" +topics = ["database"] +keywords = ["rls", "deprecated", "auth", "policy"] +--- + +## The `auth.role()` function is now deprecated + +The `auth.role()` function has been deprecated in favour of using the `TO` field, natively supported within Postgres: + +```sql +-- DEPRECATED +create policy "Public profiles are viewable by everyone." +on profiles for select using ( + auth.role() = 'authenticated' or auth.role() = 'anon' +); + +-- RECOMMENDED +create policy "Public profiles are viewable by everyone." +on profiles for select +to authenticated, anon +using ( + true +); +``` + +## The `auth.email()` function is now deprecated + +The `auth.email()` function has been deprecated in favour a more generic function to return the full JWT: + +```sql +- DEPRECATED +create policy "User can view their profile." +on profiles for select using ( + auth.email() = email +); + +-- RECOMMENDED +create policy "User can view their profile." +on profiles for select using ( + (auth.jwt() ->> 'email') = email +); +``` diff --git a/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx b/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx new file mode 100644 index 00000000000..46866f0e413 --- /dev/null +++ b/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx @@ -0,0 +1,56 @@ +--- +title = "Disabling Prepared statements" +github_url = "https://github.com/orgs/supabase/discussions/28239" +date_created = "2024-07-28T15:32:23+00:00" +topics = [ "database", "supavisor" ] +keywords = [ "prepared", "statements", "transaction", "mode", "disable" ] +database_id = "04801b69-e7eb-4f40-8d41-81110397bbc2" +--- + +### It is important to note that although the direct connections and Supavisor in session mode support prepared statements, Supavisor in transaction mode does not. + +## How to disable prepared statements for Supavisor in transaction mode + +Each ORM or library configures prepared statements differently. Here are settings for some common ones. If you don't see yours, make a comment + +# Prisma: + +add ?pgbouncer=true to end of connection string: + +``` +postgres://[db-user].[project-ref]:[db-password]@aws-0-[aws-region].pooler.supabase.com:6543/[db-name]?pgbouncer=true +``` + +# Drizzle: + +Add a prepared false flag to the client: + +```ts +export const client = postgres(connectionString, { prepare: false }) +``` + +# Node postgres + +[Just omit the "name" value in a query definition](https://node-postgres.com/features/queries#prepared-statements): + +```ts +const query = { + name: 'fetch-user', // <--------- DO NOT INCLUDE + text: 'SELECT * FROM user WHERE id = $1', + values: [1], +} +``` + +# Psycopg + +set the [prepare_threshold](https://www.psycopg.org/psycopg3/docs/api/connections.html#psycopg.Connection.prepare_threshold) to `None`. + +# asyncpg + +Follow the recommendation in the [asyncpg docs](https://magicstack.github.io/asyncpg/current/faq.html#why-am-i-getting-prepared-statement-errors) + +> disable automatic use of prepared statements by passing `statement_cache_size=0` to [asyncpg.connect()](https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.connection.connect) and [asyncpg.create_pool()](https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.pool.create_pool) (and, obviously, avoid the use of [Connection.prepare()](https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.connection.Connection.prepare)); + +# Rust's Deadpool or tokio-postgres: + +- Check [Github Discussion](https://github.com/bikeshedder/deadpool/issues/340#event-13642472475) diff --git a/apps/docs/content/troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9.mdx b/apps/docs/content/troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9.mdx new file mode 100644 index 00000000000..3df39f7060d --- /dev/null +++ b/apps/docs/content/troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9.mdx @@ -0,0 +1,351 @@ +--- +title = "Discovering and Interpreting API Errors in the Logs" +github_url = "https://github.com/orgs/supabase/discussions/22849" +date_created = "2024-04-18T02:38:44+00:00" +topics = [ "database", "platform" ] +keywords = [ "logs", "api", "errors", "postgrest", "cloudflare" ] +database_id = "188986c9-019d-4f26-baaf-6f58cec8fa7a" +--- + +> A complimentary [guide](https://github.com/orgs/supabase/discussions/26224) was made for the Postgres logs + +# Navigating the API logs: + +The Database API is powered by a [ PostgREST web-server](https://postgrest.org/en/v12/), recording every request to the API Edge Network logs. To precisely navigate them, use the [Log Explorer](https://supabase.com/dashboard/project/_/logs/explorer). These logs are managed through [Logflare](https://supabase.com/blog/supabase-logs-self-hosted) and can be queried with a subset of BigQuery SQL syntax. + +The log table that contains API requests is `edge_logs`. + +Notably, it contains: +| field | description | +--------|-------------| +| event_message | the log's message | +| timestamp | time event was recorded | +| request metadata | metadata about the REST request | +| response metadata | metadata about the REST response | + +The request and response columns are arrays in the metadata field and must be unnested. This is done with a `cross join`. + +**Unnesting example** + +```sql +select + -- the event message does not require unnesting + event_message, + -- unnested status_code column from metadata.response field + status_code +from + edge_logs + -- Unpack data stored in the 'metadata' field + cross join unnest(metadata) as metadata + -- After unpacking the 'metadata' field, extract the 'response' field from it + cross join unnest(response) as response; +``` + +The most useful fields for debugging are: + +> NOTE: not every field is included below. For a full list, check the API Edge field reference in the [Log Explorer](https://supabase.com/dashboard/project/_/logs/explorer) + +## Request object + +### Cloudflare geographic data: + +**Suggested use cases:** + +- Detecting abuse from a specific region +- Detecting activity spikes from certain regions + +| Column | Description | Sample value | +| -------------------- | --------------------- | ----------------------------------------------------- | +| request.cf.city | Requester's city | Munich | +| request.cf.country | Requester's country | [DE](https://www.iso.org/iso-3166-country-codes.html) | +| request.cf.continent | Requester's continent | EU | +| request.cf.region | Requester's region | Bavaria | +| request.cf.latitudex | Requester's latitude | 48.10840 | +| request.cf.longitude | Requester's longitude | 11.61020 | +| request.cf.timezone | Requester's timezone | Europe/Berlin | + +**Unnesting example:** + +```sql +select + city +from + edge_logs +-- Unpack 'metadata' field +cross join unnest(metadata) AS metadata +-- unpack 'request' from 'metadata' +cross join unnest(request) AS request; +-- unpack 'cf' from 'request' +cross join unnest(cf) AS cf; +``` + +### IP and browser/environment data: + +**Suggested use cases:** + +- Detecting request behavior from IP +- Detecting abuse by IP +- Detecting errors by user_agent + +| Column | Description | Sample value | +| -------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| request.headers.cf_connecting_ip | Requester's IP | 80.81.18.138 | +| request.headers.user_agent | Requester's browser or app environment | Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36 | + +**Unnesting example:** + +```sql +select + cf_connecting_ip +from + edge_logs +-- Unpack 'metadata' field +cross join unnest(metadata) AS metadata +-- unpack 'request' from 'metadata' +cross join unnest(request) AS request; +-- unpack 'headers' from 'request' +cross join unnest(headers) AS headers; +``` + +### Query type and formatting data: + +**Suggested use cases:** + +- identify problematic queries +- identify unusual behavior by authenticated users + +| Column | Description | Sample value | +| --------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| request.method | Request Method (PATCH, GET, PUT...) | GET | +| request.url | Request URL, which contains the PostgREST formatted query | https://yuhplfrsdxxxtldakizi.supabase.co/rest/v1/users?select=username&id=eq.63b6190e-214f-4b8a-b72d-3af6e1921411&limit=1 | +| request.sb.auth_users | authenticated user's ID | 63b6190e-214f-4b8a-b72d-3af6e1921411 | + +**Unnesting example:** + +```sql +select + method, + url, + auth_users +from + edge_logs +-- Unpack 'metadata' field +cross join unnest(metadata) AS metadata +-- unpack 'request' from 'metadata' +cross join unnest(request) AS request; +-- unpack 'sb' from 'request' +cross join unnest(sb) AS sb; +``` + +## Response object + +### Status code: + +**Suggested use cases:** + +- detect success/errors + +| Column | Description | Sample value | +| -------------------- | --------------------------------------- | ------------ | +| response.status_code | Response status code (200, 404, 500...) | 404 | + +**Unnesting example:** + +```sql +select + status_code +from + edge_logs + -- Unpack 'metadata' field + cross join unnest(metadata) as metadata + -- unpack 'response' from 'metadata' + cross join unnest(response) as response; +``` + +# Finding errors + +### API level errors + +The `metadata.request.url` contains PostgREST formatted queries. + +For example, the following call to the JS client: + +```js +let { data: countries, error } = await supabase.from('countries').select('name') +``` + +translates to calling the following endpoint: + +```bash +https://.supabase.co/rest/v1/countries?select=name +``` + +You can use regex ([Advanced Regex Guide](https://github.com/orgs/supabase/discussions/22640)) to find the objects related to your query. Try isolating by: + +- function names +- column names +- table names +- query methods (select, insert, ...) + +Example: + +```sql +select + cast(timestamp as datetime) as timestamp, + status_code, + url, + event_message +from edge_logs +cross join unnest(metadata) as metadata +cross join unnest(response) AS request; +cross join unnest(response) AS response; +where + -- find all errors + status_code >= 400 + and + -- find queries featuring the a specific and + ( + regexp_contains(url, '') + and + regexp_contains(event_message, '|') + ) +``` + +PostgREST has an [error reference table](https://postgrest.org/en/v12/references/errors.html) that you can use to interpret status codes. + +### Database-level errors + +However, some errors that are reported through the Database API occur at the PostgreSQL level. If it is not clear which error occurred you should reference the timestamp of the error and try to see if you can find it in the Postgres logs. + +```sql +select + cast(postgres_logs.timestamp as datetime) as timestamp, + error_severity, + user_name, + query, + detail, + sql_state_code, + event_message +from postgres_logs + cross join unnest(metadata) as metadata + cross join unnest(metadata.parsed) as parsed +where + -- filter only for error events + regexp_contains(parsed.error_severity, 'ERROR|FATAL|PANIC') + and + -- All DB API requests are registered as the authenticator role + parsed.user_name = 'authenticator' + and + -- find failed queries featuring the function + regexp_contains(parsed.query, '') + and + -- limit the time of the search to be around the time of the failed API request +postgres_logs.timestamp between '2024-04-15 10:50:00' AND '2024-04-15 10:50:27' +order by + timestamp desc +limit 100; +``` + +Like PostgREST, PostgreSQL has a [reference table](https://www.postgresql.org/docs/current/errcodes-appendix.html) for interpreting error codes. + +## PostgREST server and Cloudflare errors + +In some cases, errors may emerge because of Cloudflare or PostgREST server errors. For 500 and above errors, you may want to check your [PostgREST](https://supabase.com/dashboard/project/_/logs/postgrest-logs) logs and the [Cloudflare docs.](https://developers.cloudflare.com/support/troubleshooting/cloudflare-errors/troubleshooting-cloudflare-5xx-errors/#error-502-bad-gateway-or-error-504-gateway-timeout)) + +# Practical examples: + +**Find All Errors:** + +```sql +select + cast(timestamp as datetime) as timestamp, + status_code, + event_message, + path +from + edge_logs + cross join unnest(metadata) as metadata + cross join unnest(response) as response + cross join unnest(request) as request +where + -- find all errors + status_code >= 400 + and regexp_contains(path, '^/rest/v1/'); +-- only look at DB API +``` + +**Group errors by path and code:** + +```sql +select + status_code, + path, + count(path) as reoccurrence_per_path +from + edge_logs + cross join unnest(metadata) as metadata + cross join unnest(response) as response + cross join unnest(request) as request +where + -- find all errors + status_code >= 400 + and regexp_contains(path, '^/rest/v1/') -- only look at DB API +group by path, status_code +order by reoccurrence_per_path; +``` + +**Find requests by region:** + +```sql +select + path, + region, + count(region) as region_count +from + edge_logs + cross join unnest(metadata) as metadata + cross join unnest(request) as request + cross join unnest(cf) as cf +where + -- only look at DB API + regexp_contains(path, '^/rest/v1/') +group by region, path +order by requester_region_count; +``` + +**Find total requests by IP:** + +```sql +select + cf_connecting_ip as ip, + count(cf_connecting_ip) as ip_count +from + edge_logs + cross join unnest(metadata) as metadata + cross join unnest(request) as request + cross join unnest(headers) as headers + cross join unnest(cf) as cf + cross join unnest(response) as response +where regexp_contains(path, '^/auth/v1/') +group by ip +order by ip_count; +``` + +**Search frequented query paths by authenticated user:** + +```sql +select + -- only available for front-end clients + auth_users, + path, + count(auth_users) as ip_count +from + edge_logs + cross join unnest(metadata) as metadata + cross join unnest(request) as request + cross join unnest(sb) as sb +where + -- only look at DB API + regexp_contains(path, '^/rest/v1/') +group by auth_users, path; +``` diff --git a/apps/docs/content/troubleshooting/do-i-need-to-expose-security-definer-functions-in-row-level-security-policies-iI0uOw.mdx b/apps/docs/content/troubleshooting/do-i-need-to-expose-security-definer-functions-in-row-level-security-policies-iI0uOw.mdx new file mode 100644 index 00000000000..285b7d05e74 --- /dev/null +++ b/apps/docs/content/troubleshooting/do-i-need-to-expose-security-definer-functions-in-row-level-security-policies-iI0uOw.mdx @@ -0,0 +1,18 @@ +--- +title = 'Do I need to expose "security definer" Functions in Row Level Security Policies?' +github_url = "https://github.com/orgs/supabase/discussions/16784" +date_created = "2023-08-24T13:45:01+00:00" +topics = ["database"] +keywords = ["security", "function", "schema", "policy", "definer"] +--- + +PostgREST supports 2 config parameters: + +- Exposed Schemas +- Extra Search Path + +![image](/docs/img/troubleshooting/d756aeb0-515f-425d-b737-75a935935b73.png) + +You do not need to add your "security definer" Functions to either of these if you are using them in your Policies. + +PostgREST doesn’t need to know about this function on extra search path or exposed schemas, as long as you explicitly use the schema inside RLS (eg: `security.rls_func`). diff --git a/apps/docs/content/troubleshooting/edge-function-wall-clock-time-limit-reached-Nk38bW.mdx b/apps/docs/content/troubleshooting/edge-function-wall-clock-time-limit-reached-Nk38bW.mdx new file mode 100644 index 00000000000..1319b6d73a2 --- /dev/null +++ b/apps/docs/content/troubleshooting/edge-function-wall-clock-time-limit-reached-Nk38bW.mdx @@ -0,0 +1,43 @@ +--- +title = "Edge Function 'wall clock time limit reached'" +github_url = "https://github.com/orgs/supabase/discussions/21293" +date_created = "2024-02-15T11:42:58+00:00" +topics = [ "functions" ] +keywords = [ "wall clock", "limit", "time", "shutdown" ] +database_id = "5a3a0125-84c7-463b-a886-ca53edfba5f4" + +[[errors]] +http_status_code = 546 +message = "Edge Function 'wall clock time limit reached'" +--- + +**What Does "Wall Clock Time Limit Reached" Mean?** + +The message "wall clock time limit reached" typically indicates that a process has reached the maximum time allowed for execution. This time is measured by a clock, similar to a system clock or a clock on the wall. It encompasses the entire duration a process takes to complete, including any periods of inactivity or waiting. + +When this message appears in the context of your edge function, it means that the function has emitted a Shutdown event either after reaching the specified wall clock duration or when it hits a resource limit such as CPU time used or memory utilized. + +**Current Limits Explained** + +- Wall Clock Time Limit: Currently set at 400 seconds for the total duration your edge function can run. +- CPU Execution Time: Limited to 200 milliseconds of active computing. + +This means that if your edge function completes its task within these time constraints, there's no need to be concerned about the "wall clock time limit reached" error message. + +Because the "wall clock time limit reached" warning can be expected in some cases. This message is hard-coded to be printed out when the worker has been terminated, even if it hasn't reached the time limit. However, if your function terminates with this warning and returns a 546 error response, then this indicates that your function is exceeding the allowed execution time, signaling a long-running task. + +**Steps to Troubleshoot** +If you're facing the "wall clock time limit reached" error with a 546 error code, here are actions to take: + +- Review Your Function's Logic: Examine the operations within your edge function for any inefficiencies or prolonged processes. Consider optimizing code, minimizing unnecessary calculations, and implementing asynchronous operations where possible. + +- Divide Complex Tasks: For functions handling complex or extensive tasks, try breaking them down into smaller, discrete functions. This approach can help manage workloads more effectively and stay within time limits. + +- Monitor Execution Time: Use our logging or monitoring tools available to keep an eye on your function's performance. This can pinpoint where optimizations are necessary. To access logs visit: [Supabase Project Functions](https://app.supabase.com/project/_/functions) Select your function and click on Logs. + +- Check Our Guides: For more tips, refer to our debugging guide here: [Debugging Edge Functions](https://supabase.com/docs/guides/functions/debugging#logs--debugging) + +**Future Considerations** +There are plans to make the wall clock time limit configurable per project in the future. However, currently, the only way to adjust this limit is by self-hosting [Edge Functions](https://github.com/supabase/edge-runtime/). + +Stay updated on changes by regularly checking our changelog [here](https://github.com/orgs/supabase/discussions/categories/changelog). diff --git a/apps/docs/content/troubleshooting/error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr.mdx b/apps/docs/content/troubleshooting/error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr.mdx new file mode 100644 index 00000000000..ae4057d5efc --- /dev/null +++ b/apps/docs/content/troubleshooting/error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr.mdx @@ -0,0 +1,34 @@ +--- +title = "Error: \"Connection refused\" when trying to connect to Supabase database" +github_url = "https://github.com/orgs/supabase/discussions/14929" +date_created = "2023-06-09T10:38:06+00:00" +topics = [ "database", "cli" ] +keywords = [ "connection", "fail2ban", "econnrefused" ] +database_id = "9d8b179c-ff4d-4072-b9e6-2d16e63b2aaa" + +[api] +cli = [ "supabase-network-bans-get", "supabase-network-bans-remove" ] + +[[errors]] +message = "connect ECONNREFUSED 1.2.3.4:5432" + +[[errors]] +message = "psql: error: connection to server at \"db.xxxxxxxxxxxxxxxxxxxx.supabase.co\" (1.2.3.4), port 5432 failed: Connection refused Is the server running on that host and accepting TCP/IP connections?" +--- + +If you're not able to connect to the Supabase database and see the error `connect ECONNREFUSED 1.2.3.4:5432` or `psql: error: connection to server at "db.xxxxxxxxxxxxxxxxxxxx.supabase.co" (1.2.3.4), port 5432 failed: Connection refused +Is the server running on that host and accepting TCP/IP connections?`, this could be because there are banned IPs on your project caused by Fail2ban as it kicks in when attempting 2 wrong passwords in a row. + +These bans will clear after 30mins but you can unban the IPs using the Supabase CLI https://supabase.com/docs/guides/cli following the commands below. + +How to list the banned IPs: + +``` +% supabase network-bans get --project-ref --experimental +``` + +How to unban the IPs: + +``` +% supabase network-bans remove --db-unban-ip --project-ref --experimental +``` diff --git a/apps/docs/content/troubleshooting/error-index-row-size-exceeds-btree-version-4-maximum-for-index-LMmoeU.mdx b/apps/docs/content/troubleshooting/error-index-row-size-exceeds-btree-version-4-maximum-for-index-LMmoeU.mdx new file mode 100644 index 00000000000..bc5f7fb7ae7 --- /dev/null +++ b/apps/docs/content/troubleshooting/error-index-row-size-exceeds-btree-version-4-maximum-for-index-LMmoeU.mdx @@ -0,0 +1,67 @@ +--- +title = "Error: index row size exceeds btree version 4 maximum for index" +github_url = "https://github.com/orgs/supabase/discussions/14898" +date_created = "2023-06-08T11:35:27+00:00" +topics = [ "database" ] +keywords = [ "btree", "index", "attribute", "size", "hashing" ] +database_id = "da3aa582-b115-44e2-97e3-9ab76206f0db" + +[[errors]] +message = "index row size exceeds btree version 4 maximum 2704 for index \"idx_name\"" +--- + +## Error + +``` +index row size exceeds btree version 4 maximum 2704 for index "idx_name" +``` + +## Summary + +PG has a limit on a BTree tuple(=row) size. It needs to fit at least 3 btree tuples on a 8Kb page. That could not be changed. + +BTree row can be a single attribute or multiple attributes. These cases are better addressed separately. + +## BTree is built with multiple attributes + +BTree with multiple attributes will perform better than several only in case the likely SELECT queries use several attributes that include the first attributes that are in the index. I.e. select by 1-st, 2-nd, 3-d but not by 2-nd, 3-d and 5-th index attributes. + +The other case when multiple attributes BTree is good is when we have INSERT/UPDATE workload that is comparable to SELECT load (generally SELECTS a way more often). Then we can save speed-up updating a single index instead of several at INSERT/UPDATE at cost of SELECT performance decrease. + +But most likely we have multiple attributes BTree index due to some automation tool, not by intention. Even without the mentioned error it's best to build separate single-attribute indexes for each attribute from it. Then drop multiple attributes BTree index. This is a must and the only solution when we have this error though. + +## BTree is built on a single attribute that is very long + +This can be if the index is built on text, JSON column etc. It's not prohibited to build BTree on these datatypes, but it's also ineffective. Why? + +One of the measures of index efficiency is the ratio of index entries to the width of all possible values space for this datatype. If we have say 100000 distinct values of int32 in the index then the ratio is 1/40000. If we have text with length of 2704 bytes (maximum for BTree index) we can hardly imagine the number of distinct values that gives us even a comparable ratio. That said indexing of that long values stores much redundancy in the index. + +The solution is simple: use some king of hashing to transfer the values to much narrower space. I.e. md5. The solution is simple, you build a functional index (=index by expression): + +```sql +CREATE INDEX ON table_name(MD5(column_name)); +``` + +instead of: + +```sql +CREATE INDEX ON table_name(column_name); +``` + +Then you must modify you SELECTs to be using the same function (otherwise the functional index will not be used in SELECT queries). I.e. + +```sql +select * from table_name where MD5(column_name) = MD5('search_value'); +``` + +instead of + +```sql +select * from table_name where column_name = 'search_value'; +``` + +[More on building index by expression](https://www.postgresql.org/docs/current/sql-createindex.html) + +For some datatypes other than text that allows queries by partial inclusion (i.e. that the pair key-value is includes in a JSON or for implementing tsvector phrase search) you'd just use GIST/GIN indexes that inherently have values space much narrower that the whole to be indexed. + +[More on GIN/GiST indexes](https://www.postgresql.org/docs/15/textsearch-indexes.html) diff --git a/apps/docs/content/troubleshooting/error-invalid-totp-code-entered-CukLCj.mdx b/apps/docs/content/troubleshooting/error-invalid-totp-code-entered-CukLCj.mdx new file mode 100644 index 00000000000..5dc5634bd45 --- /dev/null +++ b/apps/docs/content/troubleshooting/error-invalid-totp-code-entered-CukLCj.mdx @@ -0,0 +1,16 @@ +--- +title = "Error: \"Invalid TOTP code entered\"" +github_url = "https://github.com/orgs/supabase/discussions/19676" +date_created = "2023-12-13T15:01:15+00:00" +topics = [ "auth" ] +keywords = [ "totp" ] +database_id = "58d5a2de-2f7e-45fc-8dfa-7fb36da41ca8" + +[api] +sdk = [ "auth-verifyotp" ] + +[[errors]] +message = "Invalid TOTP code entered" +--- + +This error would generally happen if there is an incorrect device clock setting and there is no time sync. Please check and make sure your device clock shows the correct local time, date, and time zone to work properly. diff --git a/apps/docs/content/troubleshooting/error-no-pghbaconf-entry-for-host-xxxxxxxxxxx-user-postgres-database-postgres-ssl-off-GOt5Ja.mdx b/apps/docs/content/troubleshooting/error-no-pghbaconf-entry-for-host-xxxxxxxxxxx-user-postgres-database-postgres-ssl-off-GOt5Ja.mdx new file mode 100644 index 00000000000..321e2ae8576 --- /dev/null +++ b/apps/docs/content/troubleshooting/error-no-pghbaconf-entry-for-host-xxxxxxxxxxx-user-postgres-database-postgres-ssl-off-GOt5Ja.mdx @@ -0,0 +1,17 @@ +--- +title = "error: no pg_hba.conf entry for host \"xx.xxx.xxx.xxx\", user \"postgres\", database \"postgres\", SSL off" +github_url = "https://github.com/orgs/supabase/discussions/21145" +date_created = "2024-02-09T11:48:08+00:00" +topics = [ "database", "platform" ] +keywords = [ "ssl", "authentication", "connection" ] +database_id = "0739256c-1b25-46ec-963b-2f8e4dccedc7" + +[[errors]] +message = "no pg_hba.conf entry for host \"xx.xxx.xxx.xxx\", user \"postgres\", database \"postgres\", SSL off" +--- + +This error indicates a failed authentication attempt to the database and the connection couldn't be established. + +In Supabase, this is generally seen when [SSL enforcement](https://supabase.com/docs/guides/platform/ssl-enforcement) is enabled on your Supabase Project. The authentication failed because the incoming connection didn't use SSL encryption when connecting to the database. + +You can ignore this message if the attempt is from an unknown user. If you want this connection attempt to be successful, you will either need to connect with SSL or disable SSL enforcement on your Supabase project. diff --git a/apps/docs/content/troubleshooting/error-prepared-statement-xxx-already-exists-3laqeM.mdx b/apps/docs/content/troubleshooting/error-prepared-statement-xxx-already-exists-3laqeM.mdx new file mode 100644 index 00000000000..2b5a114f58a --- /dev/null +++ b/apps/docs/content/troubleshooting/error-prepared-statement-xxx-already-exists-3laqeM.mdx @@ -0,0 +1,16 @@ +--- +title = "Error: prepared statement \"XXX\" already exists" +github_url = "https://github.com/orgs/supabase/discussions/17751" +date_created = "2023-09-27T09:53:08+00:00" +topics = [ "database" ] +keywords = [ "pgbouncer", "prepared", "connection" ] +database_id = "8688dad6-3dce-4912-9b90-4f128daf79d7" + +[[errors]] +message = "prepared statement \"XXX\" already exists" +--- + +This error occurs when you are trying to connect to the database using PgBouncer. PgBouncer does not support prepared statements. If you have prepared statements in use, you will need to use direct connections - port 5432. + +There is a special parameter in the query string for Prisma to work with PgBouncer +https://www.prisma.io/docs/guides/performance-and-optimization/connection-management/configure-pg-bouncer diff --git a/apps/docs/content/troubleshooting/failed-to-fetch-in-dashboard-and-other-areas----browser-extension-dyDTRU.mdx b/apps/docs/content/troubleshooting/failed-to-fetch-in-dashboard-and-other-areas----browser-extension-dyDTRU.mdx new file mode 100644 index 00000000000..289439862cf --- /dev/null +++ b/apps/docs/content/troubleshooting/failed-to-fetch-in-dashboard-and-other-areas----browser-extension-dyDTRU.mdx @@ -0,0 +1,25 @@ +--- +title = "Failed to Fetch in dashboard and other areas -- browser extension" +github_url = "https://github.com/orgs/supabase/discussions/17296" +date_created = "2023-09-09T13:22:26+00:00" +topics = [ "studio" ] +keywords = [ "cors" ] +database_id = "23c4e1b5-c7e7-497f-acf8-006133e243a4" + +[[errors]] +message = "Failed to Fetch" +--- + +Although Failed to Fetch can occur for a variety of reasons a common one in the dashboard or when using update code is related to a browser extension. + +![image](/docs/img/troubleshooting/7fe991ac-40ea-4628-a5e3-181d706225ea.png) + +https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf + +This extension sets PATCH to be blocked by default for some reason... + +168501713-5f020510-4555-4cc3-84de-582efc1c62de diff --git a/apps/docs/content/troubleshooting/failed-to-restore-from-backup-all-subscriptions-and-replication-slots-must-be-dropped-before-a-backup-can-be-restored-L-rCvt.mdx b/apps/docs/content/troubleshooting/failed-to-restore-from-backup-all-subscriptions-and-replication-slots-must-be-dropped-before-a-backup-can-be-restored-L-rCvt.mdx new file mode 100644 index 00000000000..38192a24eee --- /dev/null +++ b/apps/docs/content/troubleshooting/failed-to-restore-from-backup-all-subscriptions-and-replication-slots-must-be-dropped-before-a-backup-can-be-restored-L-rCvt.mdx @@ -0,0 +1,31 @@ +--- +title = "Failed to restore from backup: All subscriptions and replication slots must be dropped before a backup can be restored." +github_url = "https://github.com/orgs/supabase/discussions/21830" +date_created = "2024-03-07T02:47:01+00:00" +topics = [ "database" ] +keywords = [ "backup", "restore", "replication", "subscription" ] +database_id = "aa5fda82-5838-4975-b0ac-ba4d7fb9e638" + +[[errors]] +message = "All subscriptions and replication slots must be dropped before a backup can be restored." +--- + +As the error suggests, you must first drop any current subscriptions or replication slots to restore backups. + +You can check those with: + +``` +SELECT * FROM pg_replication_slots; + +SELECT * FROM pg_subscription; +``` + +You can drop them with: + +``` +DROP SUBSCRIPTION ; + +SELECT pg_drop_replication_slot(slot_name); +``` + +NOTE: These are destructive actions. This is fine since you will overwrite your database with a backup. diff --git a/apps/docs/content/troubleshooting/fetch-requests-to-api-endpoints-arent-showing-the-session-UbUwRs.mdx b/apps/docs/content/troubleshooting/fetch-requests-to-api-endpoints-arent-showing-the-session-UbUwRs.mdx new file mode 100644 index 00000000000..258a8b8fa00 --- /dev/null +++ b/apps/docs/content/troubleshooting/fetch-requests-to-api-endpoints-arent-showing-the-session-UbUwRs.mdx @@ -0,0 +1,18 @@ +--- +title = "Fetch requests to API endpoints aren't showing the session" +github_url = "https://github.com/orgs/supabase/discussions/15859" +date_created = "2023-07-18T12:22:14+00:00" +topics = [ "auth" ] +keywords = [ "session", "cookie" ] +database_id = "6f19b113-09cb-45ce-8141-0d6dc10ea464" +--- + +You must pass along the cookie header with the fetch request in order for your API endpoint to get access to the cookie from this request. + +```ts +const res = await fetch('http://localhost:3000/contact', { + headers: { + cookie: headers().get('cookie') as string, + }, +}) +``` diff --git a/apps/docs/content/troubleshooting/fixing-520-errors-in-the-database-rest-api-Ur5-B2.mdx b/apps/docs/content/troubleshooting/fixing-520-errors-in-the-database-rest-api-Ur5-B2.mdx new file mode 100644 index 00000000000..f1d5a150a35 --- /dev/null +++ b/apps/docs/content/troubleshooting/fixing-520-errors-in-the-database-rest-api-Ur5-B2.mdx @@ -0,0 +1,57 @@ +--- +title = "Fixing 520 Errors in the Database REST API" +github_url = "https://github.com/orgs/supabase/discussions/23422" +date_created = "2024-04-29T16:56:27+00:00" +topics = [ "database" ] +keywords = [ "520", "cloudflare", "url", "headers", "rpcs" ] +database_id = "6ec0c9a3-b52b-49b9-ba6e-c91984f7f9bf" + +[[errors]] +http_status_code = 520 +message = "Web server returns an unknown error due to request exceeding 16KB in headers/URL." +--- + +In the context of the database API, [Cloudflare 520 errors](https://developers.cloudflare.com/support/troubleshooting/cloudflare-errors/troubleshooting-cloudflare-5xx-errors/#error-520-web-server-returns-an-unknown-error) most often occur when 16+KB worth of data is present in the headers/URL of your requests. + +The API will include filters within the URL, so a request like so: + +```javascript +let { data: countries, error } = await supabase.from('countries').select('name') +``` + +translates to a URL like: + +```bash +https://.supabase.co/rest/v1/countries?select=name +``` + +However, appending too much data to the URL can exceed the 16KB limitation, triggering a 520 failure. This typically occurs with lengthy `in` clauses, as demonstrated here: + +```javascript +const { data, error } = await supabase + .from('countries') + .select() + .not('id', 'in', '(5,6,7,8,9,...10,000)') +``` + +To circumvent this issue, you must use [RPCs](https://supabase.com/docs/reference/javascript/explain?queryGroups=example&example=call-a-postgres-function-with-arguments). They are database functions that you can call from the API. Instead of including a query's structure within the URL or header, they move it into the request's payload. + +Here is a basic example of a [database function](https://supabase.com/docs/guides/database/functions) + +```sql +create or replace function example(id uuid[]) +returns uuid[] +language plpgsql +as $$ +begin + raise log 'the function example was called with an array size of: %', (select array_length(id, 1)); + return id; +end; +$$; +``` + +The [RPC](https://supabase.com/docs/reference/javascript/explain?queryGroups=example&example=call-a-postgres-function-with-arguments) can then call the function with an array that contains more than 16KB of data + +```javascript +const { data, error } = await supabase.rpc('example', { id: ['e2f34fb9-bbf9-4649-9b2f-09ec56e67a42', ...900 more UUIDs] }) +``` diff --git a/apps/docs/content/troubleshooting/forbidden-resource-error-from-the-cli-L6rm6l.mdx b/apps/docs/content/troubleshooting/forbidden-resource-error-from-the-cli-L6rm6l.mdx new file mode 100644 index 00000000000..726332b9afb --- /dev/null +++ b/apps/docs/content/troubleshooting/forbidden-resource-error-from-the-cli-L6rm6l.mdx @@ -0,0 +1,27 @@ +--- +title = "Forbidden resource error from the CLI" +github_url = "https://github.com/orgs/supabase/discussions/21602" +date_created = "2024-02-27T09:37:38+00:00" +topics = [ "cli", "platform" ] +keywords = [ "cli", "authorization", "token", "permissions" ] +database_id = "b095c3ed-23e7-4a17-9e33-d811af25ba67" + +[api] +cli = [ "supabase-link", "supabase-login" ] + +[[errors]] +http_status_code = 403 +message = "Forbidden resource error" +--- + +This error typically occurs as a protective measure to prevent unauthorized access to critical operations. + +To address this issue, we recommend following these troubleshooting steps: + +- Verify Project ID: Ensure the $PROJECT*REF variable in your commands contains the correct Project ID. You can find your Reference ID under [Project -> Settings -> General](https://supabase.com/dashboard/project/*/settings/general) in your Supabase Dashboard. A Reference ID looks something like “xvljpkujuwroxcuvossw”. +- Authorization Check: Confirm that you’ve been properly authorized. You can also generate a new Access Token in your dashboard and use it for login. Generate a new token [here](https://supabase.com/dashboard/account/tokens) and use it to [log in](https://supabase.com/docs/reference/cli/supabase-login). +- Re-link Project: Try [re-linking](https://supabase.com/docs/reference/cli/supabase-link) your project with the newly generated token. +- Owner/Admin Permissions: Make sure you have [Owner/Admin](https://supabase.com/docs/guides/platform/access-control) permissions for the project. +- CLI Version: Ensure you are using the latest version of the Supabase CLI. If not, please update to the most recent version available at [Supabase CLI GitHub](https://github.com/supabase/cli). + +If the issue persists, add a --debug --create-ticket flags to your command and contact [support](https://supabase.com/support) with the ticket id and debug logs, which can help in diagnosing the problem further. diff --git a/apps/docs/content/troubleshooting/google-auth-fails-for-some-users-XcFXEu.mdx b/apps/docs/content/troubleshooting/google-auth-fails-for-some-users-XcFXEu.mdx new file mode 100644 index 00000000000..b4deff01c56 --- /dev/null +++ b/apps/docs/content/troubleshooting/google-auth-fails-for-some-users-XcFXEu.mdx @@ -0,0 +1,50 @@ +--- +title = "Google Auth fails for some users" +github_url = "https://github.com/orgs/supabase/discussions/14883" +date_created = "2023-06-07T20:38:48+00:00" +topics = [ "auth" ] +keywords = [ "OAuth", "Google", "credential", "email" ] +database_id = "1ea7b605-dc1d-43d8-a365-5bc2801dd90e" + +[api] +sdk = [ "supabase.auth.signInWithOAuth" ] + +[[errors]] +http_status_code = 500 +code = "server_error" +message = "Error getting user email from external provider" + +[[errors]] +http_status_code = 401 +code = "UNAUTHENTICATED" +message = "Missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential." +--- + +## Google Auth fails for some users + +If you start facing either of these errors: + +``` +error=server_error&error_description=Error+getting+user+email+from+external+provider + +Missing required authentication credential. + +Expected OAuth 2 access token, login cookie or other valid authentication credential. + +See https://developers.google.com/identity/sign-in/web/devconsole-project.\",\n \"status\": \"UNAUTHENTICATED\" + + } + "level":"error","method":"GET","msg":"500: Error getting user email from external provider","path":"/callback","referer":"https://accounts.google.com/","remote_addr":"x.x.X.x","time":"2023-06-06T21:46:11Z","timestamp":"2023-06-06T21:46:11Z"} +``` + +It is happening because some Google Suite requires the explicit request of email Auth Scopes: +`https://www.googleapis.com/auth/userinfo.email` + +```js +const { data, error } = await supabase.auth.signInWithOAuth({ + provider: 'google' + options: { + scopes: 'https://www.googleapis.com/auth/userinfo.email' + } +}) +``` diff --git a/apps/docs/content/troubleshooting/grafana-not-displaying-data-sXJrMj.mdx b/apps/docs/content/troubleshooting/grafana-not-displaying-data-sXJrMj.mdx new file mode 100644 index 00000000000..b978a02e3cb --- /dev/null +++ b/apps/docs/content/troubleshooting/grafana-not-displaying-data-sXJrMj.mdx @@ -0,0 +1,83 @@ +--- +title = "Grafana not displaying data" +github_url = "https://github.com/orgs/supabase/discussions/30579" +date_created = "2024-11-21T03:45:05+00:00" +topics = [ "database" ] +keywords = [ "grafana", "docker", "metrics", "configuration" ] +database_id = "76a4099e-450f-4b5b-a539-224760348c18" +--- + +This guide is for identifying configuration mistakes in [self-hosted Supabase Grafana installations](https://supabase.com/docs/guides/monitoring-troubleshooting/metrics#deploying-supabase-grafana) + +## Step 1: Ping your Grafana endpoint + +Use the below cURL command to make sure your metrics endpoint returns data: + +```sh +curl https://.supabase.co/customer/v1/privileged/metrics --user 'service_role:' +``` + +## Step 2: Set your Grafana Dashboard to auto-refresh in the top right corner + +![388343266-ed4b8f38-e0cd-474e-bc1c-1ac6ae68e1aa](/docs/img/troubleshooting/47998bed-0b77-433a-bfed-63222beb2aee.png) + +## Step 3: Make sure your docker container has the default configurations + +Run the following command in the terminal: + +```sh +docker ps -f name=supabase-grafana +``` + +The output should look something like this: + +![image](/docs/img/troubleshooting/6c284180-0ffd-432d-b86b-e9fbcfe23868.png) + +Here it is in an easier to read format + +- CONTAINER ID: < container id > +- IMAGE: supabase-grafana-supabase-grafana +- COMMAND: /entrypoint.sh +- CREATED: < time > +- STATUS: Up < unit of time > ago +- PORTS: 3000/tcp, 0.0.0.0:8000 → 8080/tcp +- NAMES: supabase-grafana-supabase-grafana-1 + +## Step 4: Enter the container + +Try running the following terminal command: + +```sh +docker exec -it bash +``` + +## Step 5: Check the environment variables for errors + +Run the following in the docker container: + +```sh +printenv | egrep 'GRAFANA_PASSWORD|SUPABASE_PROJECT_REF|SUPABASE_SERVICE_ROLE_KEY' +``` + +Ensure the values are correct by comparing them with those in the Dashboard. Users have previously encountered issues by accidentally omitting the last character of their strings, so a thorough check is essential. + +## Step 6: Go to the root folder and check permissions on the `entrypoint.sh` file + +Run the following terminal commands: + +```sh +cd / +ls -l | grep entrypoint.sh +``` + +`entrypoint.sh` should have the following permissions: + +```sh +-rwxr-xr-x +``` + +If off, update the values + +```sh +chmod +x entrypoint.sh +``` diff --git a/apps/docs/content/troubleshooting/high-latency-with-supabase-client-z0pZzR.mdx b/apps/docs/content/troubleshooting/high-latency-with-supabase-client-z0pZzR.mdx new file mode 100644 index 00000000000..d425b632785 --- /dev/null +++ b/apps/docs/content/troubleshooting/high-latency-with-supabase-client-z0pZzR.mdx @@ -0,0 +1,99 @@ +--- +title = "High latency with supabase client" +github_url = "https://github.com/orgs/supabase/discussions/22100" +date_created = "2024-03-17T14:15:25+00:00" +topics = [ "database", "platform" ] +keywords = [ "latency", "client", "performance" ] +database_id = "a715c0d3-8636-41b7-a6da-6d49f1b1777c" +--- + +## Describe the bug + +Querying a table using the Supabase client is much slower than querying against the Postgres database directly. + +## To reproduce + +1. Execute the below DDL statements. + +``` +-- Create table +CREATE TABLE your_table_name ( + id UUID PRIMARY KEY, + column1 TEXT, + column2 INT, + column3 BOOLEAN +); + +-- Insert statements +INSERT INTO your_table_name (id, column1, column2, column3) VALUES + (uuid_generate_v4(), 'value1', 10, TRUE), + (uuid_generate_v4(), 'value2', 20, FALSE), + (uuid_generate_v4(), 'value3', 15, TRUE), + (uuid_generate_v4(), 'value4', 8, FALSE), + (uuid_generate_v4(), 'value5', 25, TRUE), + (uuid_generate_v4(), 'value6', 12, FALSE), + (uuid_generate_v4(), 'value7', 18, TRUE), + (uuid_generate_v4(), 'value8', 30, FALSE), + (uuid_generate_v4(), 'value9', 22, TRUE), + (uuid_generate_v4(), 'value10', 5, FALSE), + (uuid_generate_v4(), 'value11', 17, TRUE), + (uuid_generate_v4(), 'value12', 9, FALSE), + (uuid_generate_v4(), 'value13', 14, TRUE), + (uuid_generate_v4(), 'value14', 28, FALSE), + (uuid_generate_v4(), 'value15', 11, TRUE), + (uuid_generate_v4(), 'value16', 7, FALSE), + (uuid_generate_v4(), 'value17', 19, TRUE), + (uuid_generate_v4(), 'value18', 26, FALSE), + (uuid_generate_v4(), 'value19', 16, TRUE), + (uuid_generate_v4(), 'value20', 21, FALSE); +``` + +2. Run the following script (you may need to `pip install psycopg[binary]` in addition to supabase client. + +``` +import time +from supabase import Client, create_client + +import psycopg + + +def psycop_call(): #user_ids: list[str]): + user="YOUR_SUPABASE_USER" + password="YOUR_SUPABASE_PASSWORD" + host="SUPABASE_HOST" + port=5432 + database="postgres" + + with psycopg.connect(f"host={host} port={port} dbname={database} user={user} password={password}") as conn: + # Open a cursor to perform database operations + results = [] + with conn.cursor() as cur: + start = time.time() + # Execute a command: this creates a new table + cur.execute("SELECT * FROM public.your_table_name") + cur.fetchall() + for record in cur: + results.append(record) + stop = time.time() + return (stop - start) + + +def supabase_call(): + supabase: Client = create_client("SUPABASE_URL", "SUPBASE_SERVICE_ROLE_KEY") + start = time.time() + result = supabase.table("your_table_name").select("*").execute() + stop = time.time() + return (stop - start) + + +if __name__ == "__main__": + ref = psycop_call() + sup = supabase_call() + print(f"postgres: {ref}, supabase: {sup}, ratio: {sup/ref}") +``` + +3. You will see that the Supabase client takes longer to execute the same query, especially for smaller tables or queries returning just one row. + +## Expected behavior + +The overhead from PostgREST shouldn't be higher than a few milliseconds at max. 60-70 ms is way too high. This is particular deceiving because one can run the query on the SQL Editor page and it reports the same time as the direct postgres query, which is not what actually happens. diff --git a/apps/docs/content/troubleshooting/how-can-i-revoke-execution-of-a-postgresql-function-2GYb0A.mdx b/apps/docs/content/troubleshooting/how-can-i-revoke-execution-of-a-postgresql-function-2GYb0A.mdx new file mode 100644 index 00000000000..7ca40a0d4a3 --- /dev/null +++ b/apps/docs/content/troubleshooting/how-can-i-revoke-execution-of-a-postgresql-function-2GYb0A.mdx @@ -0,0 +1,34 @@ +--- +title = "How can I revoke execution of a PostgreSQL function?" +github_url = "https://github.com/orgs/supabase/discussions/17606" +date_created = "2023-09-21T03:04:41+00:00" +topics = [ "database", "functions" ] +keywords = [ "functions", "permissions" ] +database_id = "b7edb30b-beee-40ae-9b4f-666ef6411bb6" + +[[errors]] +message = "ERROR: permission denied for function foo" +--- + +All functions access is PUBLIC by default, this means that any role can execute it. To revoke execution, there are 2 steps required: + +- Revoke function execution (`foo` in this case) from PUBLIC: + +```sql +revoke execute on function foo from public; +``` + +- Revoke execution from a particular role (`anon` in this case): + +```sql +revoke execute on function foo from anon; +``` + +Now `anon` should get an error when trying to execute the function: + +```sql +begin; +set local role anon; +select foo(); +ERROR: permission denied for function foo +``` diff --git a/apps/docs/content/troubleshooting/how-do-i-check-gotrueapi-version-of-a-supabase-project-lQAnOR.mdx b/apps/docs/content/troubleshooting/how-do-i-check-gotrueapi-version-of-a-supabase-project-lQAnOR.mdx new file mode 100644 index 00000000000..b674f3649a1 --- /dev/null +++ b/apps/docs/content/troubleshooting/how-do-i-check-gotrueapi-version-of-a-supabase-project-lQAnOR.mdx @@ -0,0 +1,19 @@ +--- +title = "How do I check GoTrue/API version of a Supabase project?" +github_url = "https://github.com/orgs/supabase/discussions/14217" +date_created = "2023-05-09T04:21:49+00:00" +topics = [ "auth", "platform" ] +database_id = "74972531-f2fc-4d68-a745-21f9c75761e6" +--- + +Make a `GET` request to the health check endpoint to retrieve this information. Below is an example using `curl`: + +``` +curl -X GET 'https://project-ref.supabase.co/auth/v1/health' -H 'apikey: ANON_KEY' + +{ + "version": "v2.60.7", + "name": "GoTrue", + "description": "GoTrue is a user registration and authentication API" +} +``` diff --git a/apps/docs/content/troubleshooting/how-do-i-make-the-cookies-httponly-vwweFx.mdx b/apps/docs/content/troubleshooting/how-do-i-make-the-cookies-httponly-vwweFx.mdx new file mode 100644 index 00000000000..c812c28cd75 --- /dev/null +++ b/apps/docs/content/troubleshooting/how-do-i-make-the-cookies-httponly-vwweFx.mdx @@ -0,0 +1,10 @@ +--- +title = "How do I make the cookies HttpOnly?" +github_url = "https://github.com/orgs/supabase/discussions/18869" +date_created = "2023-11-10T11:49:06+00:00" +topics = [ "auth" ] +keywords = [ "cookies", "HttpOnly" ] +database_id = "cdaa76a4-3e5b-4c8f-82f0-b713c4cf19ab" +--- + +This is not necessary. Both the access token and refresh token are designed to be passed around to different components in your application. The browser-based side of your application needs access to the refresh token to properly maintain a browser session anyway. diff --git a/apps/docs/content/troubleshooting/how-do-i-reset-my-supabase-database-password-oTs5sB.mdx b/apps/docs/content/troubleshooting/how-do-i-reset-my-supabase-database-password-oTs5sB.mdx new file mode 100644 index 00000000000..b4b40b718ba --- /dev/null +++ b/apps/docs/content/troubleshooting/how-do-i-reset-my-supabase-database-password-oTs5sB.mdx @@ -0,0 +1,10 @@ +--- +title = "How do I reset my Supabase database password?" +github_url = "https://github.com/orgs/supabase/discussions/20929" +date_created = "2024-02-01T12:34:59+00:00" +topics = [ "database" ] +keywords = [ "password", "reset" ] +database_id = "6e4a515c-5e70-4b92-808c-f076d47d4e35" +--- + +You can reset your database password from the [database settings page](https://supabase.com/dashboard/project/_/settings/database) on the project dashboard. diff --git a/apps/docs/content/troubleshooting/how-do-i-update-connection-pool-settings-in-my-dashboard-wAxTJ_.mdx b/apps/docs/content/troubleshooting/how-do-i-update-connection-pool-settings-in-my-dashboard-wAxTJ_.mdx new file mode 100644 index 00000000000..32f1e91db91 --- /dev/null +++ b/apps/docs/content/troubleshooting/how-do-i-update-connection-pool-settings-in-my-dashboard-wAxTJ_.mdx @@ -0,0 +1,37 @@ +--- +title = "How do I update connection pool settings in my dashboard?" +github_url = "https://github.com/orgs/supabase/discussions/16787" +date_created = "2023-08-24T16:19:11+00:00" +topics = [ "database", "supavisor" ] +database_id = "dfefd00c-31d8-4969-86a4-03d586110f4c" +--- + +A few questions I have about updating settings for PgBouncer or Supavisor: + +- How do I know which connection pooler I'm using? + +The PgBouncer connection string looks like: `postgres://postgres:[YOUR-PASSWORD]@db.xxxxxxxxxx.supabase.co:6543/postgres` + +The Supavisor connection string looks like: `postgres://postgres.xxxxxxxxx:[YOUR-PASSWORD]@aws-0-us-west-1.pooler.supabase.com:6543/postgres` + +The subdomain will vary depending on the region a project is deployed in. The project reference is to be included in the username following a `.`. If the username is `postgres` the username you use for Supavisor is `postgres.[PROJECT_REF]`. + +- How do I update the size of the connection pool to the database? + +You can set the `Max Client Connections` field in your database settings here: + +[https://supabase.com/dashboard/project/\_/settings/database](https://supabase.com/dashboard/project/_/settings/database) + +- How do I change the client connection limit? + +You can set the `Default Pool Size` field in your database settings: + +[https://supabase.com/dashboard/project/\_/settings/database](https://supabase.com/dashboard/project/_/settings/database) + +- How do I use `session` mode? + +With Supavisor you can automatically use `session` mode by using the connection string with port `5432` in it. + +You can also set the pooler port 6543 to use `session` mode in the database settings: + +[https://supabase.com/dashboard/project/\_/settings/database](https://supabase.com/dashboard/project/_/settings/database) diff --git a/apps/docs/content/troubleshooting/how-do-you-troubleshoot-nextjs---supabase-auth-issues-riMCZV.mdx b/apps/docs/content/troubleshooting/how-do-you-troubleshoot-nextjs---supabase-auth-issues-riMCZV.mdx new file mode 100644 index 00000000000..dda68f9e16c --- /dev/null +++ b/apps/docs/content/troubleshooting/how-do-you-troubleshoot-nextjs---supabase-auth-issues-riMCZV.mdx @@ -0,0 +1,34 @@ +--- +title = "How do you troubleshoot NextJS - Supabase Auth issues?" +github_url = "https://github.com/orgs/supabase/discussions/27606" +date_created = "2024-06-28T03:26:44+00:00" +topics = [ "auth" ] +keywords = [ "NextJS", "SSR", "authentication", "client" ] +database_id = "4017d331-0b24-439a-9231-793801395ca2" +--- + +Authentication is hard. SSR is harder. At Supabase, we try to address these together by providing`@supabase/ssr` package to help implement authentication with Supabase conveniently. While I’m targeting to address NextJS-related issues, you can probably apply these concepts to other SSR frameworks such as Nuxt, SvelteKit, and Remix. This is a living document, and we plan to update it regularly as NextJS and its APIs + `@supabase/ssr` evolve. + +If you are experiencing issues with Supabase Auth and SSR, the following checklist will help you troubleshoot the issues. + +✅ You’re on the latest version of `@supabase/ssr` package. Note that the `@supabase/auth-helpers` package is being deprecated, and all the bug fixes and feature releases will be focused on the `@supabase/ssr` package. +✅ Do you have all the client utility functions implemented? This way, you can easily import them into your components that need access to Supabase auth functions. Make sure to follow the [guide available here](https://supabase.com/docs/guides/auth/server-side/creating-a-client?queryGroups=framework&framework=nextjs&queryGroups=environment&environment=client-component#creating-a-client) carefully to avoid running into unexpected errors. +✅ Do you have the `middleware.ts` file correctly implemented? This lets you refresh expired sessions before loading server components that require a user session for authorization. + +This documentation will help you validate that you’re on the correct path. + +- Setting up Server-Side Auth for Next.js - https://supabase.com/docs/guides/auth/server-side/nextjs?queryGroups=router&router=app +- SSR advanced guide - https://supabase.com/docs/guides/auth/server-side/advanced-guide +- Creating a Supabase client for SSR - https://supabase.com/docs/guides/auth/server-side/creating-a-client?queryGroups=framework&framework=nextjs&queryGroups=environment&environment=middleware + +Another way to identify any potential issues with your code is to compare it with the Supabase NextJS quickstart. Use the command `npx create-next-app -e with-supabase` to download a copy to your local machine of the quickstart application. + +Our YouTube channel has great videos to help you implement Supabase Auth with Next.js. + +- The Right Way to do Auth with the Next.js App Router - https://youtu.be/v6UvgfSIjQ0?si=TBUN9dD4pmjRg78a + +Also, please take some time to get familiar with some concepts on authentication with NextJS, such as https://nextjs.org/docs/app/building-your-application/authentication. + +We know your requirements vary, and you might run into an edge case. In that scenario, please use our amazing community channels ([GitHub](https://github.com/orgs/supabase/discussions), [Discord](https://discord.gg/rxTfewPvys)) to get help troubleshooting the issue. You can post your issues to the `@supabase/ssr` [GitHub repo](https://github.com/supabase/ssr/issues). We always welcome your contributions! + +If none of the above works, please don’t hesitate to reach out to [Supabase support](https://supabase.help); consider adding more information, such as your use case, code snippets, a copy of your `package.json`, `middleware.ts` and a HAR file, to help the support team triage your issue. diff --git a/apps/docs/content/troubleshooting/how-long-does-it-take-to-restore-a-database-from-a-point-in-time-backup-pitr-qO8gOG.mdx b/apps/docs/content/troubleshooting/how-long-does-it-take-to-restore-a-database-from-a-point-in-time-backup-pitr-qO8gOG.mdx new file mode 100644 index 00000000000..6b025149bd7 --- /dev/null +++ b/apps/docs/content/troubleshooting/how-long-does-it-take-to-restore-a-database-from-a-point-in-time-backup-pitr-qO8gOG.mdx @@ -0,0 +1,22 @@ +--- +title = "How long does it take to restore a database from a Point-in-Time backup (PITR)?" +github_url = "https://github.com/orgs/supabase/discussions/19527" +date_created = "2023-12-08T09:40:51+00:00" +keywords = [ "PITR", "restore", "WAL" ] +topics = [ "database" ] +database_id = "cc852935-510f-4beb-9f70-b4582505a863" +--- + +The time required for a PIT restoration isn't fixed. It depends on several factors: + +**Time Since Last Full Backup:** + +Full backups occur weekly. The time elapsed since the last full backup can affect restoration time. + +**Write-Ahead Logging (WAL) Activity:** + +The volume of WAL activity since the last full backup is a critical factor. More activity can lead to longer restoration times. + +**Database Size:** + +While important, the size of the database isn't the sole determinant of restoration time. diff --git a/apps/docs/content/troubleshooting/how-postgres-chooses-which-index-to-use-_JHrf4.mdx b/apps/docs/content/troubleshooting/how-postgres-chooses-which-index-to-use-_JHrf4.mdx new file mode 100644 index 00000000000..1b8f96fe639 --- /dev/null +++ b/apps/docs/content/troubleshooting/how-postgres-chooses-which-index-to-use-_JHrf4.mdx @@ -0,0 +1,162 @@ +--- +title = "How Postgres chooses which index to use" +github_url = "https://github.com/orgs/supabase/discussions/26959" +date_created = "2024-06-03T05:53:16+00:00" +topics = [ "database" ] +keywords = [ "index", "performance", "optimization" ] +database_id = "e7671f20-2639-442e-9df9-4f6fa3766598" +--- + +> For the curious: [here is a list of all built-in indexes in Postgres](https://www.postgresql.org/docs/current/indexes-types.html) + +### Postgres internals + +#### How an index is chosen + +PostgreSQL, internally, contains a few components that manage query execution: + +| Module | Description | +| ----------------- | ------------------------------------------------------------------------------------------------------------- | +| Parser | Converts SQL into an easily traversable query tree | +| Planner/Optimizer | Takes the query tree and uses rules and database statistics to find the optimal strategy for getting the data | +| Executor | Executes the plan created by the planner | + +The planner will consider using an index when an indexed column is present in a filter statement, such as: + +- `WHERE` +- `LIKE` +- `ILIKE` +- `DISTINCT` +- `SIMILAR TO` +- `JOIN` +- `ORDER BY` + +Otherwise, it will likely perform a full table scan (sequential scan). + +In the majority of cases, the indexed column must not only be present but also must be filtered by a comparison operator (`=`, `>`, `<>`) that is compatible with the index. + +As an example, one can create the following table: + +| Column Name | Data Type | +| ----------- | --------- | +| id | INT | +| data | JSONB | + +On the data column, a GIN index can be applied, which is excellent for filtering JSONB datatypes: + +```SQL +CREATE INDEX some_arbitary_index_name ON some_table USING gin (data); +``` + +Here's a [link](https://www.postgresql.org/docs/current/gist-builtin-opclasses.html) to the list operators supported by the GIN index; notably, it does not support greater than `>`: + +```sql +-- GIN index will never be used +select * +from some_table +where data -> val > 5; +``` + +GIN does support the `@>` operator: + +```sql +--GIN will be considered +SELECT id FROM some_table +WHERE data @> '[ { "itemId": "p11" } ]'; +``` + +In most cases, developers work with the default BTREE index. It is the most practical and performant in the majority of cases and is compatible with the following filter [operators](https://www.postgresql.org/docs/current/btree-behavior.html): + +| Comparison Operator | +| ------------------- | +| `<` | +| `<=` | +| `=` | +| `>=` | +| `>` | + +An operator's functional equivalents, such as `IN`, `BETWEEN`, and `ANY`, are also valid. + +However, just because the base requirements (relevant column, filter, and operators) are present, doesn't mean that an index will be used. + +Indexes have a startup cost, so for small tables, Postgres might use a sequential scan if it believes that it will take less time. The database keeps statistics about each table that it uses to inform these choices. + +In very rare cases, these statistics can become stale, and Postgres may opt to use a slower index or sequential scan when a better option is available. + +You can see the query plan with the `EXPLAIN` keyword: + +``` +EXPLAIN +``` + +To understand how to interpret its output, you can check out this [explainer](https://github.com/orgs/supabase/discussions/22839). + +To reset statistics within the database, you can use the following query: + +```sql +-- use judiciously +select pg_stat_reset(); +``` + +### Complex or composite indexes + +> For a more complete rundown, check the [Postgres Official Docs](https://www.postgresql.org/docs/current/indexes-multicolumn.html) + +#### Multi-column indexes + +If you make independent indexes on multiple columns, Postgres will likely use each of them independently to find the relevant rows and then combine the results together. + +It is possible to make [multi-column indexes](https://www.postgresql.org/docs/current/indexes-multicolumn.html). If you are regularly filtering against multiple columns, there can be performance benefits using them instead of several independent indexes. + +```sql +-- multi-column index +create index test2_mm_idx on test2 (major, minor); + +-- multi-column comparison: +select name +from test2 +where major = constant and minor = constant; +``` + +#### Ordered indexes + +If you're using an ORDER BY clause, [indexes can also be pre-sorted by DESC/ASC](https://www.postgresql.org/docs/current/indexes-ordering.html) for better performance. + +``` +-- organizes the index in a DESC order, places NULL values at the end +CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST); +``` + +#### Functional indexes + +Although not as common, indexes can also be leveraged against modified values, such as when using a LOWER function: + +```sql +-- Index on modified column through function +create index test1_lower_col1_idx on test1 (lower(col1)); + +-- Index will be considered for the following query: +select * from test1 where lower(col1) = 'value'; +``` + +#### Covering indexes + +Indexes contain pointers to a specific row, but you could instruct an index to actually hold a copy of a column's value for even faster retrieval. These are known as `covering` indexes. Because maintaining a copy is storage intensive, you should avoid using it for values with large data footprints.[ FULL VIDEO ON TOPIC](https://www.youtube.com/watch?v=bBu_V8CfWgM) + +```sql +CREATE INDEX a_b_idx ON x (a,b) INCLUDE (c); +``` + +#### Indexes on JSONB + +Although a GIN/GIST index can be used to index entire JSONB bodies, you can also target just specific Key-values with standard BTREE indexes: + +```sql +-- Example table +create table person ( + id serial primary key, + data jsonb +); + +create index index_name on person ((data ->> 'name')); +``` diff --git a/apps/docs/content/troubleshooting/how-to-change-max-database-connections-_BQ8P5.mdx b/apps/docs/content/troubleshooting/how-to-change-max-database-connections-_BQ8P5.mdx new file mode 100644 index 00000000000..6364dc49569 --- /dev/null +++ b/apps/docs/content/troubleshooting/how-to-change-max-database-connections-_BQ8P5.mdx @@ -0,0 +1,107 @@ +--- +title = "How to change max database connections" +github_url = "https://github.com/orgs/supabase/discussions/27197" +date_created = "2024-06-12T02:49:15+00:00" +topics = [ "database", "supavisor", "cli" ] +keywords = [ "connections", "postgres", "memory", "cpu" ] +database_id = "78f06ca5-6a69-4536-89fc-ed2e6ff334e5" + +[api] +cli = [ "supabase-postgres-config-update" ] +--- + +> WARNING: Manually configuring the connection count hard codes it. This means if you upgrade or downgrade your database, the connection count will not auto-resize. You will have to make sure to manually update it. + +# Changing max database connections: + +Each compute instance has a default direct connection and pooler connection settings. You can find the most recent settings in the [compute docs](https://supabase.com/docs/guides/platform/compute-add-ons#disk-io): + +| Compute Size | Direct Connections | Pooler Connections | +| ------------ | ------------------ | ------------------ | +| Nano (free) | 60 | 200 | +| Micro | 60 | 200 | +| Small | 90 | 400 | +| Medium | 120 | 600 | +| Large | 160 | 800 | +| XL | 240 | 1,000 | +| 2XL | 380 | 1,500 | +| 4XL | 480 | 3,000 | +| 8XL | 490 | 6,000 | +| 12XL | 500 | 9,000 | +| 16XL | 500 | 12,000 | + +## Configuring direct connections limits + +> Note: the Supavisor connection limits are hard-coded and cannot be changed without upgrading the compute size: + +You can configure the maximum amount of connections that Postgres will tolerate with the [Supabase CLI.](https://supabase.com/docs/guides/platform/custom-postgres-config) + +You can run the following commands: + +```sh +npx supabase login + +npx supabase --experimental --project-ref postgres-config update --config max_connections= +``` + +Then you could run the following SQL in the SQL Editor to see if the changes went through: + +```sql +SHOW max_connections; +``` + +# Dangers of increasing the direct connection limits + +**Three** factors must be taken into consideration when adjusting the direct connection limit: + +### Process schedulers and Postgres internals: + +Allowing too many direct connections in your database can overburden Postgres schedulers and other internal modules. This will result in a noticeable decrease in query throughput, despite having more connections available. EnterpriseDB wrote a wonderful [article](https://www.enterprisedb.com/postgres-tutorials/why-you-should-use-connection-pooling-when-setting-maxconnections-postgres) that outlines some of the considerations. + +The default connection values are set based on a solid understanding of Postgres architecture, and straying too far from them is _likely_ to hinder performance. However, with some experimentation, you might discover a value better suited to your specific needs. Still, unless there's a compelling reason to adjust the setting, it's generally advisable to stick with the defaults or change the values judiciously.' + +### Memory + +> If you do not know how to monitor memory and CPU with Supabase Grafana, [check here](https://github.com/orgs/supabase/discussions/27141). + +#### Each direct connection is a running process that will consume active memory + +This is a Grafana Chart of unhealthy memory usage: + +![image](/docs/img/troubleshooting/47685206-7914-440e-a010-da62f5c38186.png) + +- Yellow: represents active memory +- Red: represents SWAP, which is disk storage that the system treats as if it were actually memory +- Green: it is unclaimed (the system will always leave some memory unclaimed) +- Blue: it is cached data and a buffer + +The cache in PostgreSQL is important because the database will store frequently accessed data in it for rapid retrieval. If too much active memory is needed, it runs the risk of excessively displacing cache. This will force queries to check disk, which is slow. + +Most data in a database is idle. However, when there is little available memory or uncached data is rapidly accessed, [thrashing]() can occur. + +To avoid displacing cache or straining system resources, it is advised to not increase your direct connections unless you have a clear excess of unclaimed memory (green). + +Postgres will allow you to overcommit memory. You can run the below query to find out the hypothetical max value you could change it to without risking memory failure: + +> NOTE: You can find your server memory in the [compute add-ons docs](https://supabase.com/docs/guides/platform/compute-add-ons) + +```sql +select + '(SERVER MEMORY - ' || current_setting('shared_buffers') || ' - (' || current_setting( + 'autovacuum_max_workers' + ) || ' * ' || current_setting('maintenance_work_mem') || ')) / ' || current_setting('work_mem'); +``` + +### CPU + +The below chart is an example of what can occur to the CPU if 100s of connections are inappropriately opened/closed every second or many CPU intensive queries are run in parallel + +![image](/docs/img/troubleshooting/0e7f6842-78fd-44f9-b463-425507815fb6.png) + +If you plan on increasing your direct connection numbers, your database should have relatively predictable or low CPU usage, such as what the example displays below: + +Screenshot 2024-06-11 at 3 09 03 PM diff --git a/apps/docs/content/troubleshooting/how-to-check-if-my-queries-are-being-blocked-by-other-queries-NSKtR1.mdx b/apps/docs/content/troubleshooting/how-to-check-if-my-queries-are-being-blocked-by-other-queries-NSKtR1.mdx new file mode 100644 index 00000000000..2d08cca66f8 --- /dev/null +++ b/apps/docs/content/troubleshooting/how-to-check-if-my-queries-are-being-blocked-by-other-queries-NSKtR1.mdx @@ -0,0 +1,42 @@ +--- +title = "How to check if my queries are being blocked by other queries?" +github_url = "https://github.com/orgs/supabase/discussions/19681" +date_created = "2023-12-13T15:56:12+00:00" +topics = [ "database" ] +database_id = "990ed738-2e00-42ce-9892-77c16f870cdc" +--- + +## You can set a lock monitor view to help investigate these. + +Once you run the query that takes a long time to complete, you can go in the dashboard (or select from this view below) to check what are the blocks. + +```sql +create view + public.lock_monitor as +select + coalesce( + blockingl.relation::regclass::text, + blockingl.locktype + ) as locked_item, + now() - blockeda.query_start as waiting_duration, + blockeda.pid as blocked_pid, + blockeda.query as blocked_query, + blockedl.mode as blocked_mode, + blockinga.pid as blocking_pid, + blockinga.query as blocking_query, + blockingl.mode as blocking_mode +from + pg_locks blockedl + join pg_stat_activity blockeda on blockedl.pid = blockeda.pid + join pg_locks blockingl on ( + blockingl.transactionid = blockedl.transactionid + or blockingl.relation = blockedl.relation + and blockingl.locktype = blockedl.locktype + ) + and blockedl.pid <> blockingl.pid + join pg_stat_activity blockinga on blockingl.pid = blockinga.pid + and blockinga.datid = blockeda.datid +where + not blockedl.granted + and blockinga.datname = current_database(); +``` diff --git a/apps/docs/content/troubleshooting/how-to-delete-a-role-in-postgres-8-AvxY.mdx b/apps/docs/content/troubleshooting/how-to-delete-a-role-in-postgres-8-AvxY.mdx new file mode 100644 index 00000000000..3dd515eb6c4 --- /dev/null +++ b/apps/docs/content/troubleshooting/how-to-delete-a-role-in-postgres-8-AvxY.mdx @@ -0,0 +1,41 @@ +--- +title = "How to delete a role in Postgres" +github_url = "https://github.com/orgs/supabase/discussions/27427" +date_created = "2024-06-20T19:21:11+00:00" +topics = [ "database" ] +keywords = [ "role", "delete", "postgres" ] +database_id = "085d7649-0b5d-4c3c-b6a1-f2da4b9f33dd" + +[[errors]] +message = "a role cannot be removed while it is still referenced in any database of the cluster" +--- + +[Quote from postgres docs:](https://www.postgresql.org/docs/current/sql-droprole.html#:~:text=A%20role%20cannot%20be%20removed,been%20granted%20on%20other%20objects.) + +> A role cannot be removed if it is still referenced in any database of the cluster; an error will be raised if so. Before dropping the role, you must drop all the objects it owns (or reassign their ownership) and revoke any privileges the role has been granted on other objects. + +First make sure that Postgres has ownership over the role: + +```sql +GRANT TO "postgres"; +``` + +Then you must reassign any objects owned by role: + +```sql +REASSIGN OWNED BY TO postgres; +``` + +Once ownership is transferred, you can run the following query: + +```sql +DROP OWNED BY ; +``` + +[DROP OWNED BY](https://www.postgresql.org/docs/current/sql-drop-owned.html) does delete all objects owned by the role, which should be none. However, it also revokes the role's privileges. Once this is done, you should be able to run: + +```sql +DROP role ; +``` + +If you encounter any issues, please create a [support ticket](https://supabase.com/dashboard/support/new) diff --git a/apps/docs/content/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj.mdx b/apps/docs/content/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj.mdx new file mode 100644 index 00000000000..c1f3db9d17a --- /dev/null +++ b/apps/docs/content/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj.mdx @@ -0,0 +1,402 @@ +--- +title = "How to Interpret and Explore the Postgres Logs" +github_url = "https://github.com/orgs/supabase/discussions/26224" +date_created = "2024-05-12T21:19:37+00:00" +topics = [ "database", "platform" ] +database_id = "8b000bb4-180b-4a6c-b280-ba02965060f6" +--- + +> A complimentary guide was made for the [API logs](https://github.com/orgs/supabase/discussions/22849) + +# Debugging and monitoring Postgres with logs + +Logs provide insights into Postgres operations. They help meet compliance requirements, detect suspicious activity, and troubleshoot problems. + +## Table of contents + +- Querying Logs + - `postgres_logs` Table Structure +- Filtering Logs + - Routine Events + - By Timeframe + - By Error Severity + - By Query + - By APIs/Roles + - By Supabase Dashboard Queries + - Full Example For Finding Errors +- Logging for Compliance and Security +- Reviewing Log Settings +- Changing Log Settings + - Severity levels + - Configuring queries logged + - Logging within functions +- Frequently Asked Questions +- Other resources + +## Querying logs + +The most practical way to explore and filter logs is through the [Logs Explorer](https://supabase.com/dashboard/project/_/logs/explorer). + +It uses a subset of Bigquery SQL syntax and preparses queries for optimization. This imposes three primary limitations: + +- No subqueries or `WITH` statements +- No `*` wildcards for column names +- No `ILIKE` statements + +Although there are many strategies to filter logs, such as `like` and `in` statements, a helper function called [`regexp_contains`](https://github.com/orgs/supabase/discussions/22640) provides the most flexibility and control. + +The `postgres_logs` table contains Postgres events. + +### `postgres_logs` table structure + +The table contains 3 fundamental columns: + +| column | description | +| --------------- | ----------------------- | +| event_message | the log's message | +| timestamp | time event was recorded | +| parsed metadata | metadata about event | + +The parsed metadata column is an array that contains relevant information about events. To access the information, it must be unnested. This is done with a `cross join`. + +**Unnesting example** + +```sql +select + event_message, + parsed. +from + postgres_logs +-- Unpack data stored in the 'metadata' field +cross join unnest(metadata) AS metadata +-- After unpacking the 'metadata' field, extract the 'parsed' field from it +cross join unnest(parsed) AS parsed; +``` + +### Parsed metadata fields + +#### Query information + +| Field | Description | Example | +| --------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------- | +| parsed.query | The SQL query executed | `SELECT * FROM table;` | +| parsed.command_tag | Tag identifying the type of command (e.g., SELECT) | `SELECT`, `INSERT`, `UPDATE`... | +| parsed.internal_query | An internal query that is used to facilitate a primary query. Often used by realtime for certain tasks | `select to_jsonb()` | + +**Suggested use cases:** + +- Identifying slow queries +- Identifying failing queries + +#### Error/Warning information + +| Field | Description | Example | +| --------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| parsed.error_severity | [event severity](#severity-levels) | `LOG`, `WARNING`, `ERROR`... | +| parsed.detail | Explanation of the event according to Postgres | "Key (fk_table)=(553585367) already exists." | +| parsed.sql_state_code | An error code that maps to Postgres's error table | `42501` | +| parsed.hint | Hint on how to solve the error | "No function matches the given name and argument types. You might need to add explicit type casts." | +| parsed.context | Provides insight into where an error may have occurred | "PL/pgSQL function public.find_text(public.vector,integer) line 3 at IF" | + +**Suggested use cases:** + +- Filter by error severity or sql code +- Get hints, details, and context about error events + +#### Connection/Identification information + +| Field | Description | Example | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | +| parsed.session_id | The session ID | 12345 | +| parsed.session_start_time | The start time of the session | 2024-05-08 15:30:00 | +| parsed.connection_from | The connection IP | 192.165.1.100 | +| parsed.user_name | The name of the connecting database user | postgres | +| parsed.application_name | The name of the application | Supavisor, postgREST | +| parsed.database_name | The name of the database | postgres | +| parsed.process_id | The process ID, often used to identify extension workers | 1234 | +| parsed.backend_type | Determine if the event originated internally (e.g., from background workers like pg_net, timescale, or pg_cron) or externally from a client (`client backend`) | `client backend` | + +**Suggested use cases:** + +- Identify events by server/API +- Filter connections by IP +- Identify connections to specific databases +- Filter connections by sessions for debugging +- identify extension events + +## Filtering logs + +### Excluding routine events + +Most Postgres logs during normal periods are routine events, such as connection authorizations and checkpoints. To see the default types of events that are logged, you can check this [guide](https://gist.github.com/TheOtherBrian1/991d32c2b00dbc75d29b80d4cdf41aa7). + +When exploring the logs for atypical behavior, it's often strategic to filter out expected values. This can be done by adding the following filter to your queries: + +```sql +...query +where + -- Excluding routine events related to cron, PgBouncer, checkpoints, and successful connections + not regexp_contains(event_message, '^cron|PgBouncer|checkpoint|connection received|authenticated|authorized'; +``` + +### By timeframe + +To investigate issues around a specific period: + +```sql +-- filtering by time period +...query +where + timestamp between '2024-05-06 04:44:00' and '2024-05-06 04:45:00' +``` + +### By error severity + +This filter finds all errors, fatals, and panics: + +| Severity | Usage | +| -------- | ------------------------------------------------------------ | +| ERROR | Reports an error that caused the current command to abort. | +| FATAL | Reports an error that caused the current session to abort. | +| PANIC | Reports an error that caused all database sessions to abort. | + +```sql +-- find error events +... query +where + parsed.error_severity in ('ERROR', 'FATAL', 'PANIC') +``` + +Failure events include an sql_state_code that can be referenced in the [Postgres Docs](https://www.postgresql.org/docs/current/errcodes-appendix.html) + +### By query + +> NOTE: Unless pg_audit is configured, only failed queries are logged + +``` +-- find queries executed by the Dashboard +...query +where + regexp_contains(parsed.query, '(?i)select . ') +``` + +Queries can use complex syntax, so it is often helpful to isolate by referenced database objects, such as `functions`, `tables`, and `columns`. Because query structures can be complex, it is advised to use [regex](https://github.com/orgs/supabase/discussions/22640) to find matches. Some common regex patterns are: + +- `(?i)`: ignore case sensitivty +- `.`: wildcard +- `^`: look for values at start of string +- `|`: or operator + +## By APIs/roles + +All failed queries, including those from PostgREST, Auth, and external libraries (e.g., Prisma) are logged with helpful error messages for debugging. + +#### Server/Role mapping + +API servers have assigned database roles for connecting to the database: + +| Role | API/Tool | +| -------------------------- | ------------------------------------------------------------------------- | +| supabase_admin | Used by Supabase to configure projects and for monitoring | +| authenticator | PostgREST | +| supabase_auth_admin | Auth | +| supabase_storage_admin | Storage | +| supabase_realtime_admin | Realtime | +| supabase_replication_admin | Synchronizes Read Replicas | +| postgres | Supabase Dashboard and External Tools (e.g., Prisma, SQLAlchemy, PSQL...) | +| Custom roles | External Tools (e.g., Prisma, SQLAlchemy, PSQL...) | + +Filter by the `parsed.user_name` role to only retrieve logs made by specific roles: + +```sql +-- find events based on role/server +... query +where + -- find events from the relevant role + parsed.user_name = '' +... +``` + +## By Dashboard queries + +Queries from the Supabase Dashboard are executed under the postgres role and include the comment `-- source: dashboard`. To isolate or exclude Dashboard requests during debugging, you can filter by this comment. + +```sql +-- find queries executed by the Dashboard +...query +where + regexp_contains(parsed.query, '-- source: dashboard') +``` + +## Full example for finding errors + +```sql +select + cast(postgres_logs.timestamp as datetime) as timestamp, + event_message, + parsed.error_severity, + parsed.user_name, + parsed.query, + parsed.detail, + parsed.hint, + parsed.sql_state_code, + parsed.backend_type +from + postgres_logs + cross join unnest(metadata) as metadata + cross join unnest(metadata.parsed) as parsed +where + regexp_contains(parsed.error_severity, 'ERROR|FATAL|PANIC') + and parsed.user_name = 'postgres' + and regexp_contains(event_message, 'duration|operator') + and not regexp_contains(parsed.query, '') + and postgres_logs.timestamp between '2024-04-15 10:50:00' and '2024-04-15 10:50:27' +order by timestamp desc +limit 100; +``` + +# Logging for compliance and security + +### Customized object and role activity logging + +> ⚠️ NOTE: This is specifically designated for those using the `postgres` role or [custom roles](https://supabase.com/docs/guides/database/postgres/roles) to interact with their database. Those utilizing the Database REST API should reference the [Database API Logging Guide](https://github.com/orgs/supabase/discussions/22849) instead. + +When recording what is accessed and by whom, logging based on database roles and objects is the most reliable way to ensure a proper trail of activity. + +You can use the [pg_audit](https://supabase.com/docs/guides/database/extensions/pgaudit) extension to selectively log relevant queries (not just errors) by certain roles, against specific database objects. + +You should take care when using the extension to not log all database events, but only what is absolutely necessary. Overlogging can strain the database and create log noise that makes it difficult to filter for relevant events. + +**Filtering by pg_audit**: + +```sql +... query +where + -- all pg_audit recorded events start with 'AUDIT' + regexp_contains(event_message, '^AUDIT') + and + -- Finding queries executed from the relevant role (e.g., 'API_role') + parsed.user_name = 'API_role' +``` + +### Filtering by IP + +> If you are connecting from a known, limited range of IP addresses, you should enable [network restrictions](https://supabase.com/docs/guides/platform/network-restrictions). + +Monitoring IPs becomes tricky when dealing with dynamic addressing, such as those from serverless or edge environments. This challenge amplifies when relying on certain poolers, such as Prisma Accelerate, Supavisor, or Cloudflare's Hyperdrive, as they record the pooler's IP, not the true origin. + +IP tracking is most effective when consistently relying on direct database connections from servers with static IP addresses: + +```sql +-- filter by IP +select + event_message, + connection_from as ip, + count(connection_from) as ip_count +from + postgres_logs + cross join unnest(metadata) as metadata + cross join unnest(parsed) as parsed +where + regexp_contains(user_name, '') + and regexp_contains(backend_type, 'client backend') -- only search for connections from outside the database (excludes cron jobs) + and regexp_contains(event_message, '^connection authenticated') -- only view successful authentication events +group by connection_from, event_message +order by ip_count desc +limit 100; +``` + +# Reviewing log settings + +The `pg_settings` table describes system and logging configurations. + +```sql +-- view system variables +select * from pg_settings; +``` + +The settings that affect logs are categorized under: +| Category | Description | +|----------|-------------| +| `Reporting and Logging / What to Log` | Specifies system events worth logging.| +| `Reporting and Logging / When to Log` | Specifies certain conditions or rules for logging +| `Customized Options` | Configures extensions and loaded modules, including those enhancing logging like auto_explain and pg_audit. | + +To view all log settings for your database, you can execute the following SQL: + +```sql +-- view all log related settings +select * +from pg_settings +where + ( + category like 'Reporting and Logging / What to Log' + or category like 'Reporting and Logging / When to Log' + or category = 'Customized Options' + ) + and name like '%log%'; +``` + +## Changing log settings + +> WARNING: lenient settings can lead to overlogging, impacting database performance while creating noise in the logs. + +#### Severity levels + +The `log_min_messages` variable determines what is severe enough to log. Here are the severity thresholds from the [Postgres docs](https://www.postgresql.org/docs/current/runtime-config-logging.html). + +| Severity | Usage | +| ---------------- | ---------------------------------------------------------------------------------------------------- | +| DEBUG1 .. DEBUG5 | Provides successively-more-detailed information for use by developers. | +| INFO | Provides information implicitly requested by the user, e.g., output from VACUUM VERBOSE. | +| NOTICE | Provides information that might be helpful to users, e.g., notice of truncation of long identifiers. | +| WARNING | Provides warnings of likely problems, e.g., COMMIT outside a transaction block. | +| ERROR | Reports an error that caused the current command to abort. | +| LOG | Reports information of interest to administrators, e.g., checkpoint activity. | +| FATAL | Reports an error that caused the current session to abort. | +| PANIC | Reports an error that caused all database sessions to abort. | + +In most cases, the default is adequate. However, if you must adjust the setting, you can do so with the following query: + +```sql +alter role postgres set log_min_messages = ''; + +-- view new setting +show log_min_messages; -- default WARNING +``` + +#### Configuring queries logged + +By default, only failed queries are logged. The [PGAudit extension](https://supabase.com/docs/guides/database/extensions/pgaudit) extends Postgres's built-in logging abilities. It can be used to selectively track all queries in your database by: + +- role +- session +- database object +- entire database + +#### Logging within database functions + +To track or debug functions, logging can be configured by followin the [function debugging guide](https://supabase.com/docs/guides/database/functions#general-logging) + +# Frequently Asked Questions + +#### Can I join together different log tables, such as `edge_logs` and `postgres_logs`? + +No, log tables are independent from each other and do not share any primary/foreign key relations for joining. + +#### Can I download the logs? + +At the moment, the way to download logs is through the Log Dashboard as a CSV + +#### What is logged? + +To see the default types of events that are logged, you can check this [guide](https://gist.github.com/TheOtherBrian1/991d32c2b00dbc75d29b80d4cdf41aa7). + +### Other resources: + +- [Regex for filtering logs](https://github.com/orgs/supabase/discussions/22640) +- [Debugging with the DB API logs](https://github.com/orgs/supabase/discussions/22849) +- [Debugging Database Functions](https://supabase.com/docs/guides/database/functions#debugging-functions) +- [pg_audit](https://supabase.com/docs/guides/database/extensions/pgaudit) +- [Supabase Logging](https://supabase.com/docs/guides/platform/logs) +- [Self-Hosting Logs](https://supabase.com/docs/reference/self-hosting-analytics/introduction) diff --git a/apps/docs/content/troubleshooting/how-to-migrate-from-supabase-auth-helpers-to-ssr-package-5NRunM.mdx b/apps/docs/content/troubleshooting/how-to-migrate-from-supabase-auth-helpers-to-ssr-package-5NRunM.mdx new file mode 100644 index 00000000000..1985eaac3b7 --- /dev/null +++ b/apps/docs/content/troubleshooting/how-to-migrate-from-supabase-auth-helpers-to-ssr-package-5NRunM.mdx @@ -0,0 +1,316 @@ +--- +title = "How to Migrate from Supabase Auth Helpers to SSR package" +github_url = "https://github.com/orgs/supabase/discussions/27849" +date_created = "2024-07-07T17:24:30+00:00" +topics = [ "auth" ] +keywords = [ "migration", "auth", "ssr", "package" ] +database_id = "273b79ad-b8ff-4669-8544-b99920e2d3c8" +--- + +The `auth-helpers` packages are being deprecated and replaced with the `@supabase/ssr` package. We recommend migrating to the `@supabase/ssr` package as future bug fixes and feature releases are focused on the `@supabase/ssr` package. + +Here are the steps for you to migrate your application from the `auth-helpers` package to `@supabase/ssr` package. + +Depending on your implementation, you may ignore some parts of this documentation and use your own implementation (i.e. using API routes vs. Server Actions). What’s important is you replace the clients provided by `auth-helpers` with the utility functions created using clients provided by `@supabase/ssr`. + +### 1. Uninstall Supabase Auth helpers and install the Supabase SSR package + +It’s important that you don’t use both `auth-helpers-nextjs` and `@supabase/ssr` packages in the same application to avoid running into authentication issues. + +``` +npm uninstall @supabase/auth-helpers-nextjs @supabase/supabase-js +npm install @supabase/ssr @supabase/supabase-js +``` + +### 2. Create the utility functions to create Supabase clients. + +``` +// utils/supabase/client.ts + +import { createBrowserClient } from '@supabase/ssr'; + +export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); +} + +// utils/supabase/server.ts +import { createServerClient, type CookieOptions } from '@supabase/ssr'; +import { cookies } from 'next/headers'; + +export function createClient() { + const cookieStore = cookies(); + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll() + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options) + ) + } catch { + // The `setAll` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + } + ); +} + +// utils/supabase/middleware.ts +import { createServerClient } from '@supabase/ssr'; +import { NextResponse, type NextRequest } from 'next/server'; + +export async function updateSession(request: NextRequest) { + let supabaseResponse = NextResponse.next({ + request, + }); + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll() + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value)) + supabaseResponse = NextResponse.next({ + request, + }) + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options) + ) + }, + }, + } + ); + + // IMPORTANT: Avoid writing any logic between createServerClient and + // supabase.auth.getUser(). A simple mistake could make it very hard to debug + // issues with users being randomly logged out. + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if ( + !user && + !request.nextUrl.pathname.startsWith('/login') && + !request.nextUrl.pathname.startsWith('/auth') + ) { + // no user, potentially respond by redirecting the user to the login page + const url = request.nextUrl.clone(); + url.pathname = '/login'; + return NextResponse.redirect(url); + } + + // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're + // creating a new response object with NextResponse.next() make sure to: + // 1. Pass the request in it, like so: + // const myNewResponse = NextResponse.next({ request }) + // 2. Copy over the cookies, like so: + // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll()) + // 3. Change the myNewResponse object to fit your needs, but avoid changing + // the cookies! + // 4. Finally: + // return myNewResponse + // If this is not done, you may be causing the browser and server to go out + // of sync and terminate the user's session prematurely! + + return supabaseResponse; +} +``` + +### 3. Replace your middleware.ts file + +``` +// middleware.ts + +import { type NextRequest } from 'next/server'; +import { updateSession } from '@/utils/supabase/middleware'; + +export async function middleware(request: NextRequest) { + return await updateSession(request); +} + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * Feel free to modify this pattern to include more paths. + */ + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +}; +``` + +### 4. Create your server actions to handle login and sign up. + +``` +// app/login/actions.ts + +'use server'; + +import { revalidatePath } from 'next/cache'; +import { redirect } from 'next/navigation'; + +import { createClient } from '@/utils/supabase/server'; + +export async function login(formData: FormData) { + const supabase = createClient(); + + // type-casting here for convenience + // in practice, you should validate your inputs + const data = { + email: formData.get('email') as string, + password: formData.get('password') as string, + }; + + const { error } = await supabase.auth.signInWithPassword(data) + + if (error) { + redirect('/error'); + } + + revalidatePath('/', 'layout'); + redirect('/'); +} + +export async function signup(formData: FormData) { + const supabase = createClient(); + + // type-casting here for convenience + // in practice, you should validate your inputs + const data = { + email: formData.get('email') as string, + password: formData.get('password') as string, + }; + + const { error } = await supabase.auth.signUp(data); + + if (error) { + redirect('/error'); + } + + revalidatePath('/', 'layout'); + redirect('/'); +} +``` + +### 5. Utilize the server actions in your login page UI. + +``` +// app/login/page.tsx + +import { login, signup } from './actions'; + +export default function LoginPage() { + return ( +
+ + + + + + +
+ ); +} +``` + +### 6. Client components + +``` +'use client'; + +// replace this line +import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; + +// with +import { createClient } from '@/utils/supabase/client'; + +export default async function Page() { + // replace this line + const supabase = createClientComponentClient(); + + // with + const supabase = createClient(); + + return... +} +``` + +### 7. Server components + +``` +// replace +import { cookies } from 'next/headers'; +import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'; + +// with +import { createClient } from '@/utils/supabase/server'; + +export default async function Page() { + // replace + const cookieStore = cookies(); + const supabase = createServerComponentClient({ + cookies: () => cookieStore + }); + + // with + const supabase = createClient(); + + return... +} +``` + +### 8. Route handlers + +``` +// replace +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; +import { cookies } from 'next/headers'; + +// with +import { createClient } from '@/utils/supabase/server'; + +export async function POST(request: Request) { + // replace + const supabase = createRouteHandlerClient({ + cookies: () => cookieStore, + }); + + // with + const supabase = createClient(); + + return... +} +``` + +Likewise, you can replace the clients created with `@supabase/auth-helpers-nextjs` with utility functions you created with `@supabase/ssr`. + +`createMiddlewareClient` → `createServerClient` +`createClientComponentClient` → `createBrowserClient` +`createServerComponentClient` → `createServerClient` +`createRouteHandlerClient` → `createServerClient` + +You can find more clear and concise examples of creating clients in our documentation [here](https://supabase.com/docs/guides/auth/server-side/creating-a-client?queryGroups=framework&framework=nextjs&queryGroups=environment&environment=route-handler#creating-a-client). + +If you have any feedback about this guide, please provide them as a comment below. If you find any issues or have feedback for the `@supabase/ssr` client, please post them as an issue in `@supabase/ssr` repo. + +As always, our GitHub community and Discord channel are open for technical discussions and resolving your issues. diff --git a/apps/docs/content/troubleshooting/how-to-view-database-metrics-uqf2z_.mdx b/apps/docs/content/troubleshooting/how-to-view-database-metrics-uqf2z_.mdx new file mode 100644 index 00000000000..7c6d18e0968 --- /dev/null +++ b/apps/docs/content/troubleshooting/how-to-view-database-metrics-uqf2z_.mdx @@ -0,0 +1,12 @@ +--- +title = "How to View Database Metrics" +github_url = "https://github.com/orgs/supabase/discussions/20938" +date_created = "2024-02-01T17:48:11+00:00" +topics = [ "database", "platform" ] +keywords = [ "metrics", "grafana", "monitoring" ] +database_id = "da2d95e5-abc5-47c8-8389-1554d12abf91" +--- + +To monitor real-time metrics of your database, like CPU, EBS, active database connections, and memory usage, you can deploy a Grafana Dashboard. Check our [GitHub repo](https://github.com/supabase/supabase-grafana) for setup instructions for local or free [Fly.io](http://fly.io/) deployments. Refer to our concise [documentation](https://supabase.com/docs/guides/platform/metrics) to learn more about the metrics endpoint. + +While the [Dashboard's Reports Page](https://supabase.com/dashboard/project/_/reports) displays some metric data, it provides hourly averages, not real-time by the second data. However, it offers query metrics, which the Grafana Dashboard does not include. diff --git a/apps/docs/content/troubleshooting/i-am-not-receiving-password-reset-emails-for-supabase-dashboard--cO5yf.mdx b/apps/docs/content/troubleshooting/i-am-not-receiving-password-reset-emails-for-supabase-dashboard--cO5yf.mdx new file mode 100644 index 00000000000..7044de530fc --- /dev/null +++ b/apps/docs/content/troubleshooting/i-am-not-receiving-password-reset-emails-for-supabase-dashboard--cO5yf.mdx @@ -0,0 +1,10 @@ +--- +title = "I am not receiving password reset emails for Supabase dashboard" +github_url = "https://github.com/orgs/supabase/discussions/14043" +date_created = "2023-04-30T08:08:07+00:00" +topics = [ "auth", "platform" ] +keywords = [ "email", "reset" ] +database_id = "ec816bde-1a27-461f-89ef-211e96810dd9" +--- + +Most likely, you signed up on Supabase using a GitHub account. Please try the GitHub login method to see if you can log in. If you still cannot log in, please write to support@supabase.com using your email with a screenshot of the error from the dashboard. diff --git a/apps/docs/content/troubleshooting/importing-stripe-or-other-modules-from-esmsh-on-deno-edge-functions-throws-an-error-TmbB5p.mdx b/apps/docs/content/troubleshooting/importing-stripe-or-other-modules-from-esmsh-on-deno-edge-functions-throws-an-error-TmbB5p.mdx new file mode 100644 index 00000000000..272dd4a326a --- /dev/null +++ b/apps/docs/content/troubleshooting/importing-stripe-or-other-modules-from-esmsh-on-deno-edge-functions-throws-an-error-TmbB5p.mdx @@ -0,0 +1,14 @@ +--- +title = "Importing Stripe or other modules from esm.sh on Deno Edge Functions throws an error" +github_url = "https://github.com/orgs/supabase/discussions/14303" +date_created = "2023-05-12T01:30:31+00:00" +topics = [ "functions" ] +keywords = [ "import", "module", "esm" ] +database_id = "225cb141-f442-47cf-834d-1d82d703748a" +--- + +Try adding `?target=deno` to the import path of the module. For Stripe, the updated import would be: + +``` +import Stripe from "https://esm.sh/stripe@11.2.0?target=deno"; +``` diff --git a/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx b/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx new file mode 100644 index 00000000000..1c639a31d35 --- /dev/null +++ b/apps/docs/content/troubleshooting/increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM.mdx @@ -0,0 +1,113 @@ +--- +title = "Increase vector lookup speeds by applying an HSNW index" +github_url = "https://github.com/orgs/supabase/discussions/21379" +date_created = "2024-02-20T05:32:05+00:00" +topics = [ "database", "ai", "platform" ] +keywords = [ "index", "hnsw", "vector", "performance", "speed" ] +database_id = "7d755701-747f-4c3a-b8be-236c5518e4eb" +--- + +> Although this guide is specifically for HSNW indexes, it can be generalized to work for any index type + +> Building an index without the `CONCURRENTLY` modifier will lock the table, but it will also increase build times. For general advice about indexes, check out this [guide](https://github.com/orgs/supabase/discussions/22449). + +### **To speed up queries, it is ideal to create an HSNW index on your embedded column** + +The general structure for creating an hsnw index follows this pattern: + +```sql +CREATE INDEX ON USING hnsw ( ); +``` + +Search can be one of three types: +operator | description | search type +-- | -- | -- +`<->` | Euclidean distance | vector_l2_ops +`<#>` | negative inner product | vector_ip_ops +`<=>` | cosine distance | vector_cosine_ops + +Queries can only utilize the index if it matches the search type used. If you are unsure which search type to prioritize, vector_cosine_ops is the most commonly used. You can checkout our [guide](https://supabase.com/docs/guides/ai/vector-indexes/hnsw-indexes) for more info. The folks at Crunchy Data also wrote an [explainer](https://www.crunchydata.com/blog/hnsw-indexes-with-postgres-and-pgvector) that you may find useful. + +Applying an index can be slow and computationally expensive, so there are a few preparations that should be made beforehand: + +**1. Make sure your pgvector is the latest available version on Supabase.** + +Versions 0.6 and later have accelerated HNSW build speeds. You can observe your current version in the [Dashboard's Extensions Page](https://supabase.com/dashboard/project/_/database/extensions). You can perform a software upgrade in the [Infrastructure Settings ](https://supabase.com/dashboard/project/_/settings/infrastructure)if necessary. + +**2. Setting up an external connection** + +The Dashboard has an internal time limit of ~2 minutes for queries. Indexing a large table will almost always take more time, so it is necessary to execute your code through an external interface, such as PSQL. + +You can install PSQL in [macOS](https://stackoverflow.com/a/49689589/2188186) and [Windows](https://www.postgresql.org/download/windows/) by following these links and instructions. +For Linux (Debian) you can run the following: + +```bash +sudo apt-get update +sudo apt-get install postgresql-client +``` + +Once installed, you can find your PSQL string from the [Database Settings](https://supabase.com/dashboard/project/_/settings/database), which can be executed in the terminal to create a psql session. + +If your network can use IPv6, consider using the direct connection string instead of Supavisor. It's not mandatory, but for tasks that run a long time, it's best to reduce network complexity. To check if your network is compatible, use this cURL command to request your IPv6 address: + +```bash +curl -6 https://ifconfig.co/ip +``` + +If an address is returned, you should be able to use your direct connection string found in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database): + +**3. Increase memory for index creation (optional)** + +The `maintance_work_mem` variable limits the maximum amount of memory that can be used by maintenance operations, such as vacuuming, altering, and indexing tables. In your session you should try to set it to a reasonably high value: + +```sql +set maintenance_work_mem to ; -- '#GB' +``` + +Inspect value to make sure it has been set: + +```sql +show maintenance_work_mem; +``` + +**4. Increase cores for index creation (optional)** + +The `max_parallel_maintenance_workers` variable limits the amount of cores that can be used by maintenance operations, including indexing tables. In your session, you should try to set it to a value roughly 1/2 to 2/3s of your [compute core amount](https://supabase.com/docs/guides/platform/compute-add-ons): + +```sql +set max_parallel_maintenance_workers to ; +``` + +Inspect value to make sure it has been set: + +```sql +show max_parallel_maintenance_workers; +``` + +**5. Setting a custom timeout** + +Disable query timeout for your connection: + +```sql +set statement_timeout = '0'; +``` + +Inspect value to make sure it has been set: + +```sql +show statement_timeout; +``` + +**6. Consider temporarily upgrading your compute size (optional)** + +If your task is particularly long, you can speed it up by boosting your computing power temporarily. Compute size is charged by the hour, so you can increase it for an hour or two to finish your task faster, then scale it back afterward. Here is a list of [compute add-ons](https://supabase.com/docs/guides/platform/compute-add-ons). If you want to temporarily upgrade, you can find the add-ons for your project in your [Dashboard's Add-Ons Settings.](https://supabase.green/dashboard/project/_/settings/addons) + +**7. Consider increasing disk size (optional)** + +HSNW indexes can produce temporary files during their construction that may consume a few GBs worth of disk. Consider increasing the disk size in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) to accommodate for short-term disk stress. + +Screenshot 2024-06-10 at 8 00 28 PM diff --git a/apps/docs/content/troubleshooting/inserting-into-sequenceserial-table-causes-duplicate-key-violates-unique-constraint-error-pi6DnC.mdx b/apps/docs/content/troubleshooting/inserting-into-sequenceserial-table-causes-duplicate-key-violates-unique-constraint-error-pi6DnC.mdx new file mode 100644 index 00000000000..62d23123f6a --- /dev/null +++ b/apps/docs/content/troubleshooting/inserting-into-sequenceserial-table-causes-duplicate-key-violates-unique-constraint-error-pi6DnC.mdx @@ -0,0 +1,39 @@ +--- +title = "Inserting into Sequence/Serial Table Causes \"duplicate key violates unique constraint\" Error" +github_url = "https://github.com/orgs/supabase/discussions/21134" +date_created = "2024-02-09T03:10:39+00:00" +topics = [ "database" ] +keywords = [ "duplicate", "sequence", "table", "key", "constraint" ] +database_id = "a1156bbe-289e-4462-a32d-2b0ca03691e4" + +[[errors]] +message = "duplicate key violates unique constraint" +--- + +If you are receiving the below error for an auto-incremented table: + +> ERROR: duplicate key violates unique constraint + +it likely means that the table's sequence has somehow become out of sync, likely because of a mass import process (or something along those lines). + +Call it a "bug by design", but it seems that you have to manually reset the a primary key index after restoring from a dump file. + +You can run the following commands on your instance to see if your sequence is out-of-sync: + +```sql +SELECT MAX() FROM ; + +SELECT nextval(pg_get_serial_sequence('', '')); +``` + +If the values are off by more than 1, you need to resynchronize your sequence. + +Back up your PG database by restarting in the [General Settings](https://supabase.com/dashboard/project/_/settings/general) (just in case). When you restore your database, you will have a backup saved. Alternatively, you can also just download your properties table instead as a backup. + +Then you can run this: + +```sql +SELECT SETVAL('public.__seq', (SELECT MAX() FROM )+1); +``` + +That will set the sequence to the next available value that's higher than any existing primary key in the sequence. diff --git a/apps/docs/content/troubleshooting/inspecting-edge-function-environment-variables-wg5qOQ.mdx b/apps/docs/content/troubleshooting/inspecting-edge-function-environment-variables-wg5qOQ.mdx new file mode 100644 index 00000000000..775a47275ee --- /dev/null +++ b/apps/docs/content/troubleshooting/inspecting-edge-function-environment-variables-wg5qOQ.mdx @@ -0,0 +1,71 @@ +--- +title = "Inspecting edge function environment variables" +github_url = "https://github.com/orgs/supabase/discussions/27139" +date_created = "2024-06-09T18:44:40+00:00" +topics = [ "functions", "cli" ] +keywords = [ "logging", "environment", "variables", "secrets", "docker" ] +database_id = "f47c9b0e-f537-4908-8d35-5dd3dc4c5627" + +[api] +cli = [ "supabase-init", "supabase-secrets-set", "supabase-secrets-list" ] +--- + +Sometimes it can be informative to log values from your Edge Functions. This walks you through the process of logging environment variables for inspection, but it can be generalized for all logging. + +**Steps:** + +1. enable docker + +2. Create a local Supabase project + +```bash +npx supabase init +``` + +3. create a .env file in the supabase folder + +```bash +echo "MY_NAME=Some_name" >> ./supabase/.env +``` + +4. deploy the newly added secret + +```bash +npx supabase secrets set --env-file ./supabase/.env --project-ref +``` + +5. Run the following CLI command to check secrets: + +``` +npx supabase secrets list +``` + +For security reasons, it is not advised to log secrets, but you can log a truncated version just for the reassurance that they're being updated: + +```typescript +//logs the function call and the secrets +console.log('Hello from Functions!') + +//custom secret +console.log('logging custom secret', Deno.env.get('MY_NAME')) + +// default secrets +console.log('logging SUPABASE_URL:', Deno.env.get('SUPABASE_URL').slice(0, 15)) + +Deno.serve(async (req) => { + const { name } = await req.json() + const data = { + message: `Hello ${name}!`, + } + + return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } }) +}) +``` + +After calling your function, you can check your [edge function logs](https://supabase.com/dashboard/project/_/functions/hello-world/logs?s=logging) to observe the logged values. It should look something like this: + +> Note: search filters are case sensitive and must be present in the event message. + +![image](/docs/img/troubleshooting/a360b417-e0cc-4706-8df4-89af63dcdc70.png) + +> Note: excessively long JSON logs may be truncated. If this occurs, use the [JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) function to convert the JSON object into text. You can then copy and paste the log into a [JSON beautifier.](https://jsonformatter.org/) diff --git a/apps/docs/content/troubleshooting/insufficient-privilege-when-accessing-pgstatstatements-e5M_EQ.mdx b/apps/docs/content/troubleshooting/insufficient-privilege-when-accessing-pgstatstatements-e5M_EQ.mdx new file mode 100644 index 00000000000..af72fe2edce --- /dev/null +++ b/apps/docs/content/troubleshooting/insufficient-privilege-when-accessing-pgstatstatements-e5M_EQ.mdx @@ -0,0 +1,19 @@ +--- +title = "\"insufficient privilege\" when accessing pg_stat_statements" +github_url = "https://github.com/orgs/supabase/discussions/20126" +date_created = "2024-01-03T10:59:49+00:00" +topics = [ "database" ] +keywords = [ "privilege", "permissions", "postgres" ] +database_id = "1da315be-38ac-487a-b178-f865a1a73c09" + +[[errors]] +message = "insufficient privilege" +--- + +If you see the error "insufficient privilege" when accessing [pg_stat_statements](https://supabase.com/docs/guides/platform/performance#postgres-cumulative-statistics-system) or when accessing [Query Performance Report](https://supabase.com/dashboard/project/_/reports/query-performance), it means that the Postgres role does not have required permissions. + +In this case, you can run the below command to allow the Postgres role to read all statistics from the system: + +``` +grant pg_read_all_stats to postgres; +``` diff --git a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx new file mode 100644 index 00000000000..e74e03c479a --- /dev/null +++ b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx @@ -0,0 +1,39 @@ +--- +title = "Interpreting Supabase Grafana CPU charts" +github_url = "https://github.com/orgs/supabase/discussions/27022" +date_created = "2024-06-05T04:46:27+00:00" +topics = [ "platform", "database" ] +keywords = [ "cpu", "grafana", "metrics" ] +database_id = "ef05da0a-f8bc-44a4-9719-5ae811dba104" +--- + +> [Guide](https://supabase.com/docs/guides/monitoring-troubleshooting/metrics#deploying-supabase-grafana) for setting up Supabase Grafana + +# CPU + +Here are examples of unhealthy CPU utilization: + +![image](/docs/img/troubleshooting/e7d78109-b09d-48ca-8cc6-e4913693e163.png) +![image](/docs/img/troubleshooting/49e175d9-6338-4b17-97be-4bf7853e319f.png) + +The CPU chart shows 4 distinct metrics of interest: + +- **Yellow**: It represents CPU utilized in [kernel space](https://en.wikipedia.org/wiki/User_space_and_kernel_space) (privileged OS operations). If this is high, it may be a sign that your app is connecting/disconnecting too aggressively. It could also be symptomatic of extension-related errors. +- **Blue**: It represents requests in user space and mainly reflects the CPU usage from regular queries. For optimization tips, check out the links at the end of the page. +- **Red**: It represents cycles the CPU spent idle because it was waiting on IO tasks. Any amount of red often implies [disk](https://github.com/orgs/supabase/discussions/27003) or, indirectly, [memory](https://github.com/orgs/supabase/discussions/27021) problems. The links outline how to address this type of issue. +- **Green**: These are CPU cycles spent idle. + +As the CPU peaks towards 100%, queries and database tasks will begin to throttle, as they won't have enough time or access to the CPU. + +### Other useful Supabase Grafana guides: + +- [Connections](https://github.com/orgs/supabase/discussions/27141) +- [Disk](https://github.com/orgs/supabase/discussions/27003) +- [Memory](https://github.com/orgs/supabase/discussions/27021) + +### Optimizing: + +1. [Optimize your queries](https://supabase.com/docs/guides/database/query-optimization). +2. [Add indexes](https://github.com/orgs/supabase/discussions/22449) if possible. +3. [Increasing the compute size](https://supabase.com/docs/guides/platform/compute-add-ons) +4. [Distribute load by using read-replicas](https://supabase.com/dashboard/project/_/settings/infrastructure) diff --git a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx new file mode 100644 index 00000000000..1af1847e25e --- /dev/null +++ b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx @@ -0,0 +1,95 @@ +--- +title = "Interpreting Supabase Grafana IO charts" +github_url = "https://github.com/orgs/supabase/discussions/27003" +date_created = "2024-06-04T15:32:14+00:00" +topics = [ "database", "platform" ] +keywords = [ "io", "disk", "database", "grafana" ] +database_id = "0056cd40-df04-4045-bbfb-c245cb15b85d" +--- + +> [Supabase Grafana Installation Guide](https://supabase.com/docs/guides/platform/metrics#deploying-supabase-grafana) + +There are two primary values that matter for IO: + +- **Disk Throughput**: how much data can be moved to and from disk per second +- **IOPS(Input/Output per second)**: how many read/write requests can be performed against your disk per second + +Each compute instance has unique IO settings. The settings at the time this was written are listed below. +| Plan | Baseline Disk Throughput | Baseline IOPS | +|---------------|--------------------------|---------------| +| Nano (free) | 43 Mbps | 250 IOPS | +| Micro | 87 Mbps | 500 IOPS | +| Small | 174 Mbps | 1,000 IOPS | +| Medium | 347 Mbps | 2,000 IOPS | +| Large | 630 Mbps | 3,000 IOPS | +| XL | 1,048 Mbps | 3,000 IOPS | +| 2XL | 1,048 Mbps | 3,000 IOPS | +| 4XL | 1,048 Mbps | 3,000 IOPS | +| 8XL | 1,048 Mbps | 3,000 IOPS | +| 12XL | 1,048 Mbps | 3,000 IOPS | +| 16XL | 1,048 Mbps | 3,000 IOPS | + +Compute sizes below XL, to accommodate their limitations, have burst budgets. This is when instances are allowed to utilize 1048 Mbps of disk throughput and 3,000 IOPS for 30 minutes before returning back to their baseline behavior. + +There are other metrics that indicate IO strain. + +This example shows a 16XL database exhibiting severe IO strain: + +![image](/docs/img/troubleshooting/9c6e0f9c-f842-463b-b214-1a9e31de3b2c.png) + +Its Disk IOPS is constantly near peak capacity: + +![image](/docs/img/troubleshooting/ec5ed538-985b-411d-a0d7-be718b28d367.png) + +Its throughput is also high: + +![image](/docs/img/troubleshooting/90cb6a70-fc67-4666-9217-6fa37cfacf82.png) + +As a side-effect, its CPU is encumbered by heavy Busy IOWait activity: + +![image](/docs/img/troubleshooting/9b88a738-65d6-4be4-8fa0-bc3f13a9a408.png) + +Excessive IO usage is highly problematic as it clarifies that your database is expending more IO than it normally is intended to manage. This can be caused by + +- **Excessive and needless sequential scans:** poorly indexed tables force requests to scan disk ([guide to resolve](https://github.com/orgs/supabase/discussions/22449)) +- **Too little cache**: There is not enough memory, so instead of reading data from the memory cache, it is accessed from disk ([guide to inspect](https://github.com/orgs/supabase/discussions/22449)) +- **Poorly optimized RLS policies**: RLS that rely heavily on joins are more likely to hit disk. If possible, they should optimized ([RLS best practice guide](https://supabase.com/docs/guides/database/postgres/row-level-security#rls-performance-recommendations)) +- **Excessive bloat**: This is the least likely to cause major issues, but bloat can take up space, preventing data on disk from being placed in the same locality. This can force the database to scan more pages than necessary. ([explainer guide](https://supabase.com/blog/postgres-bloat)) +- **Uploading high amounts of data:** temporarily increase compute add-on size for the duration of the uploads +- **Insufficient memory**: Sometimes an inadequate amount of memory forces queries to hit disk instead of the memory cache. Address memory issues ([guide](https://github.com/orgs/supabase/discussions/27021)) can reduce disk strain. + +If a database exhibited some of these metrics for prolonged periods, then there are a few primary approaches: + +- Scale the database to get more IO if possible +- Optimize queries/tables or refactor database/app to reduce IO +- Spin-up a [read-replica](https://supabase.com/dashboard/project/_/settings/infrastructure) +- Modify IO configs in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) +- [Partitions](https://supabase.com/docs/guides/database/partitions): Generally should be used on very large tables to minimize data pulled from disk + +Other useful Supabase Grafana guides: + +- [Connections](https://github.com/orgs/supabase/discussions/27141) +- [Memory](https://github.com/orgs/supabase/discussions/27021) +- [CPU](https://github.com/orgs/supabase/discussions/27022) + +### Esoteric factors + +**Webhooks**: +Supabase webhooks use the pg_net extension to handle requests. The `net.http_request_queue` table isn't indexed to keep write costs low. However, if you upload millions of rows to a webhook-enabled table too quickly, it can significantly increase the read costs for the extension. + +To check if reads are becoming expensive, run: + +```sql +select count(*) as exact_count from net.http_request_queue; +-- the number should be relatively low <20,000 +``` + +If you encounter this issue, you can either: + +1. Increase your compute size to help handle the large volume of requests. + +2. Truncate the table to clear the queue: + +```sql +TRUNCATE net.http_request_queue; +``` diff --git a/apps/docs/content/troubleshooting/jwt-expired-error-in-supabase-dashboard-F06k3x.mdx b/apps/docs/content/troubleshooting/jwt-expired-error-in-supabase-dashboard-F06k3x.mdx new file mode 100644 index 00000000000..9a2a8cb53bb --- /dev/null +++ b/apps/docs/content/troubleshooting/jwt-expired-error-in-supabase-dashboard-F06k3x.mdx @@ -0,0 +1,15 @@ +--- +title = "\"JWT Expired\" Error in Supabase Dashboard" +github_url = "https://github.com/orgs/supabase/discussions/21135" +date_created = "2024-02-09T04:03:23+00:00" +topics = [ "studio" ] +keywords = [ "JWT" ] +database_id = "362840be-a99c-4a98-bf4b-6e44a399af12" + +[[errors]] +message = "JWT expired" +--- + +When the Dashboard issues JWT expired errors, it usually means your computer's time is not in sync with the actual time. + +Go to https://time.is/ to compare your computer's clock with your local time. If your computer's time is inaccurate, sync it to resolve the issue diff --git a/apps/docs/content/troubleshooting/lost-accessforgot-the-mfa-device-nAPT-7.mdx b/apps/docs/content/troubleshooting/lost-accessforgot-the-mfa-device-nAPT-7.mdx new file mode 100644 index 00000000000..0ff6b9fa9c2 --- /dev/null +++ b/apps/docs/content/troubleshooting/lost-accessforgot-the-mfa-device-nAPT-7.mdx @@ -0,0 +1,18 @@ +--- +title = "Lost access/Forgot the MFA device" +github_url = "https://github.com/orgs/supabase/discussions/23183" +date_created = "2024-04-23T12:52:13+00:00" +topics = [ "auth", "platform" ] +keywords = [ "mfa", "security" ] +database_id = "60bd4a35-fc2e-4e42-bcf6-a430d380c930" +--- + +**Non-SSO User** + +For now, we [will not be able to restore access to your account](https://supabase.com/docs/guides/platform/multi-factor-authentication#enable-mfa) if you lose all your two-factor authentication credentials. Register a backup factor if necessary. + +If you forgot the MFA authentication method used, you can reach out to [our support](https://supabase.com/dashboard/support/new). + +**SSO User** + +Reach out to [our support](https://supabase.com/dashboard/support/new) and we will assist you further. diff --git a/apps/docs/content/troubleshooting/memory-and-swap-usage-explained-aPNgm0.mdx b/apps/docs/content/troubleshooting/memory-and-swap-usage-explained-aPNgm0.mdx new file mode 100644 index 00000000000..68c68f04d2a --- /dev/null +++ b/apps/docs/content/troubleshooting/memory-and-swap-usage-explained-aPNgm0.mdx @@ -0,0 +1,56 @@ +--- +title = "Memory and Swap usage explained" +github_url = "https://github.com/orgs/supabase/discussions/21460" +date_created = "2024-02-22T19:01:27+00:00" +topics = [ "database", "platform" ] +keywords = [ "memory", "swap", "ram", "disk", "IO" ] +database_id = "43b62743-252c-4069-a719-0c0737839fd9" +--- + +## Understanding swap and memory usage + +- Swap is part of Linux's tiered memory system +- Serves as a backup memory space when RAM is limited. +- System prioritizes keeping actively used data in RAM +- Data is temporarily cached in memory for quick access +- Swap is used for less frequently accessed or non-critical data +- Default swapiness value of 60 indicates the system's preference to fill up swap for performance benefits + +## Compute instance configuration + +- Default swapiness: 60 (out of 100) + - Determines how aggressively the system moves data from RAM to swap + - Default value in Linux distributions + - Changing the swapiness value significantly impacts system behavior, as it is a non-linear value. A small adjustment of 10 can result in a very different system behavior +- Swap provision: 1024MB on every compute instance + +## Issues with high swap usage + +- When swap reaches its limits, the system faces decisions: + 1. Evict cached memory from swap, which affects disk performance + 2. Evict cached memory from the system's memory cache, leading to increased disk accesses + 3. Access the disk directly without caching, resulting in more frequent disk reads +- Swap usage reaching limits indicates running out of free memory + +## Disk balance and AWS limits + +- Each project has a baseline 'balance replenishment rate' +- The replenishment rate is higher on greater AWS instances +- Disk IO usage below replenishment rate keeps balance at 99% +- Disk IO usage above replenishment rate decreases balance +- Balance increases when IO usage is below replenishment rate +- AWS does not export relevant metrics for tracking IO usage + +## Monitoring recommendations + +- Monitor memory usage: + - If usage is over 85% and swap usage is over 90% for an extended period, optimize database access or add more resources +- Monitor CPU iowait usage and disk IO metrics: + - Exported metrics here: **https://github.com/supabase/grafana-agent-fly-example/blob/main/metrics.md** + - Key metrics to track excessive disk IO usage include: + - `node_disk_reads_completed_total`: Tracks the total number of completed disk reads. + - `node_disk_io_time_seconds_total`: Measures the total time spent on disk IO operations. + - `node_disk_io_now`: Indicates the current disk IO operations. + - Differentiate system and data disks using the 'device' label + - System disk: `{device="nvme0n1"}` (contains swap) + - Data disk: `{device="nvme1n1"}` (dedicated to Postgres data) diff --git a/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx b/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx index c202f6509bb..787c3331675 100644 --- a/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx +++ b/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx @@ -14,6 +14,7 @@ This guide explains how connections impact your Supabase database's performance Supabase has an [open-source Grafana Repo](https://github.com/supabase/supabase-grafana) that displays real-time metrics of your database. Although the [Reports Dashboard ](/dashboard/project/_/reports) provides similar metrics, it averages the data by the hour or day. Having visuals of your connection usage can help you better allocate resources. _Visual of Grafana Dashboard_ + ![image](https://github.com/supabase/supabase/assets/91111415/18ed2c88-332e-4e66-b9b4-c37e99a39104) It can be run locally within Docker. Alternatively, you can deploy it to fly.io or Grafana Cloud, which are better for long-term data collection. @@ -23,6 +24,7 @@ Installation instructions can be found in it the [metrics docs ](/docs/guides/pl ## Observing connections In Supabase Grafana, the "Client Connections" graph shows connections to both Supavisor and Postgres + ![image](https://github.com/supabase/supabase/assets/91111415/141b8e01-9aae-4544-818c-acb47371ef3b) - **Yellow**: The yellow line represents the number of actively querying and idle connections to the Supavisor Pooler. @@ -81,6 +83,7 @@ If you believe a connection should be killed, you can do so by running the follo The Supavisor Pooler is an intermediary between your clients (application servers) and the database. In transaction mode (port 6543), it can enable Postgres to share single connections with many clients, only allowing access when a query is pending. This prevents idle clients from hogging a direct connection and allows for more throughput. In cases where you see significantly more pooler connections than direct connections, if you can, you should consider increasing how many direct connections the pooler is allowed to manage in the [Dashboard's Database Settings](/dashboard/project/_/settings/database): + ![image](https://github.com/supabase/supabase/assets/91111415/8e0cc80a-4d46-44b5-915b-8d7549f352d3). The general rule is that if you are using the PostgREST database API, you should avoid raising your pool size past 40%. Otherwise, you can commit 80% to the pool. This leaves adequate room for the Authentication server and other utilities. diff --git a/apps/docs/content/troubleshooting/new-variable-is-null-in-a-trigger-function--l9AOZ.mdx b/apps/docs/content/troubleshooting/new-variable-is-null-in-a-trigger-function--l9AOZ.mdx new file mode 100644 index 00000000000..09ffe5768d7 --- /dev/null +++ b/apps/docs/content/troubleshooting/new-variable-is-null-in-a-trigger-function--l9AOZ.mdx @@ -0,0 +1,18 @@ +--- +title = "NEW variable is null in a trigger function." +github_url = "https://github.com/orgs/supabase/discussions/15934" +date_created = "2023-07-20T19:05:52+00:00" +topics = [ "database", "functions" ] +keywords = [ "trigger", "function", "statement", "row", "NEW", "OLD", "SQL" ] +database_id = "f5cabb97-aae3-4fb5-b597-4e7857821753" +--- + +When you create a function and call it from a trigger the NEW variable is only passed in if the trigger is on ROW operations + +The UI currently defaults to STATEMENT which will not provide NEW or OLD variables to the function. + +![image](/docs/img/troubleshooting/49e11154-296b-42b9-8036-8a7288e49b8a.png) + +Also in SQL (reference: https://www.postgresql.org/docs/current/sql-createtrigger.html) + +![image](/docs/img/troubleshooting/c7ac0f69-6fb9-4ff2-8aab-5cda6bada3f6.png) diff --git a/apps/docs/content/troubleshooting/nextjs-1314-stale-data-when-changing-rls-or-table-data-85b8oQ.mdx b/apps/docs/content/troubleshooting/nextjs-1314-stale-data-when-changing-rls-or-table-data-85b8oQ.mdx new file mode 100644 index 00000000000..4f2a5b4a174 --- /dev/null +++ b/apps/docs/content/troubleshooting/nextjs-1314-stale-data-when-changing-rls-or-table-data-85b8oQ.mdx @@ -0,0 +1,21 @@ +--- +title = "Next.js 13/14 stale data when changing RLS or table data." +github_url = "https://github.com/orgs/supabase/discussions/19084" +date_created = "2023-11-19T19:33:06+00:00" +topics = [ "auth", "platform" ] +keywords = [ "cache", "rls", "next.js" ] +database_id = "9be3727b-c3ea-4dda-b30c-55f3c8aa8f35" +--- + +Next.js caches urls in certain cases. This is causing users to lose lots of time debugging early on with RLS changes. Changing the table data in the UI will also not be returned in these cases. + +You can look at the Dashboard API Edge log to see if the request is making it to Supabase. +Another way to check if the url caching is impacting you is to change `.select('*')` to `.select('colname')` or change column names. This would bust the next.js cache. + +Users have suggested the following three options to turn off the caching, but please refer to next.js docs as needed. + +`export const dynamic = 'force-dynamic'; // no caching` +or +`export const fetchCache = 'force-no-store' // to page.js` +or +`export const revalidate = 0 ` diff --git a/apps/docs/content/troubleshooting/no-toast-messages-on-the-dashboard-BrvP8h.mdx b/apps/docs/content/troubleshooting/no-toast-messages-on-the-dashboard-BrvP8h.mdx new file mode 100644 index 00000000000..86d61556a2d --- /dev/null +++ b/apps/docs/content/troubleshooting/no-toast-messages-on-the-dashboard-BrvP8h.mdx @@ -0,0 +1,38 @@ +--- +title = "No toast messages on the Dashboard" +github_url = "https://github.com/orgs/supabase/discussions/27702" +date_created = "2024-07-01T19:45:53+00:00" +topics = [ "studio" ] +keywords = [ "notifications", "toasts" ] +database_id = "a1bb1131-dd11-4a76-8a70-98a666c08286" +--- + +This is a known issue that is on the roadmap to resolve. However, this discussion serves as a temporary workaround for affected users until then. + +## What is the issue? + +When executing operations in the Supabase dashboard, there are situations where you will not see a toast message in the top right corner to indicate the status of the operation. This leaves users confused about the operation's status and why it errored. They would then have to look in the browser console logs for further details. + +Here is an example of a toast error message: + +![Screenshot 2024-07-01 at 2 05 36 PM](/docs/img/troubleshooting/8ba54441-2057-4dc0-8add-a8eaab9f2d93.png) + +Here is an example of a toast success message: + +![Screenshot 2024-07-01 at 2 06 35 PM](/docs/img/troubleshooting/c6e228b8-fa64-47a9-bdf6-f1f6405b9cf9.png) + +## What causes it? + +If you are not seeing these error messages, there are a couple of known scenarios that might be blocking them: + +1. **Brave Browser**: By default, Brave has strict ad-blocking settings that can block these messages. + +2. **Chrome Browser**: Specifically, the ARM64 build (tested on v125.0.6422.142) with the new v3 manifest version of uBlock Origin Lite ([https://ublockorigin.com/](https://ublockorigin.com/)). If the setting is changed from basic to optimal, it can block these messages. + +There are likely other causes that are not known right now as well. + +## How to fix this issue? + +**Adjust Ad-blocking Settings**: Turn down the ad-blocking settings or turn it off for the Supabase dashboard. + +**Use a different Browser**: If the above is not feasible, using a different browser is an alternative. diff --git a/apps/docs/content/troubleshooting/not-receiving-auth-emails-from-the-supabase-project-OFSNzw.mdx b/apps/docs/content/troubleshooting/not-receiving-auth-emails-from-the-supabase-project-OFSNzw.mdx new file mode 100644 index 00000000000..1c65d8d2619 --- /dev/null +++ b/apps/docs/content/troubleshooting/not-receiving-auth-emails-from-the-supabase-project-OFSNzw.mdx @@ -0,0 +1,29 @@ +--- +title = "Not receiving Auth emails from the Supabase project" +github_url = "https://github.com/orgs/supabase/discussions/22610" +date_created = "2024-04-10T05:18:51+00:00" +topics = [ "auth", "platform" ] +keywords = [ "smtp", "email", "delivery", "logs", "provider", "firewalls" ] +database_id = "98e3b318-ad9a-41f5-ada0-44b61970ab4f" +--- + +**We strongly recommend configuring your own custom SMTP provider** in your projects to avoid running into issues with sending emails from your project. The built-in email provider is for demonstration purposes only and offers a very low rate limit. You can read more details about the rate limits [here](https://supabase.com/docs/guides/platform/going-into-prod#auth-rate-limits). + +If you're testing your Supabase projects, consider configuring an email testing tool like [Mailtrap](https://mailtrap.io/) to test your emails. It's an email sandbox tool that helps debug emails without actually sending an email to an end user. + +As the initial step in debugging not-delivered emails, check your project's [Auth logs](https://supabase.com/dashboard/project/_/logs/auth-logs) for errors that can occur when handing over to the email providers. In general, handover issues can occur due to misconfiguration of the custom SMTP provider. + +Once handed over to the email provider, Supabase has no control over email delivery. There can be multiple reasons why emails do not reach the user's inbox. + +#### 1. Issues with the email provider. + +The email provider's logs are the next place to look for email delivery issues. There are cases where the email provider blocks the delivery due to past bounced-back emails. Some email providers maintain a suppression list for not sending emails due to several reasons. You can read more about it here - https://sendgrid.com/en-us/blog/what-is-a-suppression-list + +#### 2. Issues with the user's email server + +Many email firewalls maintain a denylist of IPs and domain names for security reasons and as spam filters. Sometimes, they block incoming emails from unknown addresses with certain keywords such as password reset, verification link, etc. In these cases, ask the user to check with their email server admin to see if they are blocking your email domain or quarantining the incoming emails. If you use the default provider, the email domain is `supabase.io`. Some information on email firewalls are available here - https://mailchimp.com/help/about-email-firewalls/. + +I haven't mentioned the most obvious cases, like checking a user's spam inbox. However, It's worth noting that sometimes this can happen with a default provider. It's also one of the reasons that you should configure your own email provider where you have more control over email delivery. + +References: +Configure a Custom SMTP - https://supabase.com/docs/guides/auth/auth-smtp diff --git a/apps/docs/content/troubleshooting/oauth-sign-in-isnt-redirecting-on-the-server-side-ShGMtr.mdx b/apps/docs/content/troubleshooting/oauth-sign-in-isnt-redirecting-on-the-server-side-ShGMtr.mdx new file mode 100644 index 00000000000..d0f4876f07d --- /dev/null +++ b/apps/docs/content/troubleshooting/oauth-sign-in-isnt-redirecting-on-the-server-side-ShGMtr.mdx @@ -0,0 +1,46 @@ +--- +title = "OAuth sign in isn't redirecting on the server side" +github_url = "https://github.com/orgs/supabase/discussions/15862" +date_created = "2023-07-18T12:28:55+00:00" +topics = [ "auth" ] +keywords = [ "oauth", "redirects", "server-side" ] +database_id = "3e9246cb-d592-4051-93c8-53e4e555711c" +--- + +The reason behind this limitation is that the auth helpers library lacks a direct mechanism for performing server-side redirects, as each framework handles redirects differently. However, the library does offer a URL through the data property it returns, which should be utilized for the purpose of redirection. + +**NextJS:** + +```ts +import { NextResponse } from "next/server"; +... +const { data } = await supabase.auth.signInWithOAuth({ + provider: 'github', +}) + +return NextResponse.redirect(data.url) +``` + +**SvelteKit:** + +```ts +import { redirect } from '@sveltejs/kit'; +... +const { data } = await supabase.auth.signInWithOAuth({ + provider: 'github', +}) + +throw redirect(303, data.url) +``` + +**Remix:** + +```ts +import { redirect } from "@remix-run/node"; // or cloudflare/deno +... +const { data } = await supabase.auth.signInWithOAuth({ + provider: 'github', +}) + +return redirect(data.url) +``` diff --git a/apps/docs/content/troubleshooting/partitioning-an-existing-table-with-same-name-VEnbzK.mdx b/apps/docs/content/troubleshooting/partitioning-an-existing-table-with-same-name-VEnbzK.mdx new file mode 100644 index 00000000000..300ef52dd87 --- /dev/null +++ b/apps/docs/content/troubleshooting/partitioning-an-existing-table-with-same-name-VEnbzK.mdx @@ -0,0 +1,34 @@ +--- +title = "Partitioning an existing table with same name" +github_url = "https://github.com/orgs/supabase/discussions/21380" +date_created = "2024-02-20T08:39:24+00:00" +topics = [ "database" ] +keywords = [ "partitioning", "table", "postgresql", "constraint", "index" ] +database_id = "3142f418-028e-4ab1-8f82-a7096be90422" +--- + +Our documentation on table partitioning here: https://supabase.com/docs/guides/database/partitions + +Read the below information if you want the partitioned table to retain the name of the original table. + +- You can create empty "parent" partitioned table using CREATE TABLE parent_table LIKE source_table [ like_option ... ] PARTITION BY ... + +LIKE option will copy column definitions, constraints etc. from source table automatically. For different like_options see https://www.postgresql.org/docs/current/sql-createtable.html (you could try INCLUDING ALL like option). + +- Then you can create empty partitions for ranges that you like. + +- Then you could insert rows from the old/original table into parent_table. They will be spread into partitions at almost no cost. But the process could take time. For experiment, you could insert a subset from original table (using `ORDER BY id LIMIT ` clause) and see time needed to do actual work. + +- It's crucial to check that you haven't forgotten some range by checking default partition (that contains rows inserted into a parent table but not corresponding any partition ranges) meanwhile. Overgrown default partition slows down almost any operations with partitioned table. + +- When insert is finished you can rename old and new "parent" table. Do it in one transaction, so that you have no time old name is already don't exist but not yet re-created for any query. + +- Indices definition will be copied with INCLUDING ALL option. They will be propagated during insertion of data into a new parent table. + +- Of course you can hold old table for your data to be safe indefinitely. Do not delete it outright! Your queries after renaming will not use it. Indexes etc. work behind the scenes, you are not required to know its names. (In reality indices for a "new" partitioned table will be new, but you won't see it if you have used LIKE..INCLUDING ALL option). + +- Triggers will be needed to re-create for "parent" partitioned table using `CREATE TRIGGER` clause https://www.postgresql.org/docs/16/sql-createtrigger.html + +- Functions will switch to work with a new table after renaming. + +- There are certain limitations of declarative partitioning that you may need to be aware: https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE diff --git a/apps/docs/content/troubleshooting/pausing-pro-projects-vNL-2a.mdx b/apps/docs/content/troubleshooting/pausing-pro-projects-vNL-2a.mdx new file mode 100644 index 00000000000..5ceae8850dc --- /dev/null +++ b/apps/docs/content/troubleshooting/pausing-pro-projects-vNL-2a.mdx @@ -0,0 +1,31 @@ +--- +title = "Pausing Pro-Projects" +github_url = "https://github.com/orgs/supabase/discussions/27399" +date_created = "2024-06-19T23:24:18+00:00" +topics = [ "platform", "cli", "storage" ] +keywords = [ "pause", "projects", "backup", "storage" ] +database_id = "76b2e2b6-8f1d-448a-a3dd-cdf68755d1a9" + +[api] +cli = [ "supabase-login", "supabase-link", "supabase-init", "supabase-storage-cp" ] +--- + +Pro-Projects at the moment cannot paused. However, [You are allowed to have two free organizations](https://supabase.com/docs/guides/platform/org-based-billing#mix-of-free-and-pro-plan-projects) that can support one active project each and an unlimited amount of paused ones. + +If a project is under 500MB, you can [transfer it to be under a free organization](https://supabase.com/docs/guides/platform/project-transfer). Afterward, you can initiate a pause. + +Alternatively, you can download a [daily backup](https://supabase.com/dashboard/project/_/database/backups/scheduled) of just your database for archiving. You can also manually download a .SQL file of your database and storage buckets by following this [guide](https://supabase.com/docs/guides/platform/migrating-and-upgrading-projects#migrate-your-project). + +You can also download your storage buckets with the [Supabase CLI:](https://supabase.com/docs/guides/cli/getting-started?queryGroups=platform&platform=npx) + +```sql +npx supabase login + +# link to your project +npx supabase link + +npx supabase init + +# will download files to current folder +npx supabase storage cp -r ss://bucket . --experimental +``` diff --git a/apps/docs/content/troubleshooting/performing-administration-tasks-on-the-server-side-with-the-servicerole-secret-BYM4Fa.mdx b/apps/docs/content/troubleshooting/performing-administration-tasks-on-the-server-side-with-the-servicerole-secret-BYM4Fa.mdx new file mode 100644 index 00000000000..d8387878832 --- /dev/null +++ b/apps/docs/content/troubleshooting/performing-administration-tasks-on-the-server-side-with-the-servicerole-secret-BYM4Fa.mdx @@ -0,0 +1,26 @@ +--- +title = "Performing administration tasks on the server side with the service_role secret" +github_url = "https://github.com/orgs/supabase/discussions/15860" +date_created = "2023-07-18T12:25:03+00:00" +topics = [ "auth", "platform" ] +keywords = [ "service_role", "server", "security" ] +database_id = "0d3389c0-5e75-473c-a18b-0699d0911fb2" +--- + +By default, the auth-helpers/ssr do not permit the use of the `service_role` `secret`. This restriction is in place to prevent the accidental exposure of your `service_role` `secret` to the public. Since the auth-helpers/ssr function on both the server and client side, it becomes challenging to separate the key specifically for client-side usage. + +However, there is a solution. You can create a separate Supabase client using the `createClient` method from `@supabase/supabase-js` and provide it with the `service_role` `secret`. In a server environment, you will also need to disable certain properties to ensure proper functionality. + +By implementing this approach, you can safely utilize the `service_role` `secret` without compromising security or exposing sensitive information to the public. + +```ts +import { createClient } from '@supabase/supabase-js' + +const supabase = createClient(supabaseUrl, serviceRoleSecret, { + auth: { + persistSession: false, + autoRefreshToken: false, + detectSessionInUrl: false, + }, +}) +``` diff --git a/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx b/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx new file mode 100644 index 00000000000..07a9d832e03 --- /dev/null +++ b/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx @@ -0,0 +1,199 @@ +--- +title = "pg_cron debugging guide" +github_url = "https://github.com/orgs/supabase/discussions/30168" +date_created = "2024-10-29T15:12:11+00:00" +topics = [ "database" ] +keywords = [ "pg_cron", "cron", "jobs", "scheduler", "debugging" ] +database_id = "9a55c946-877f-46ae-8b57-51934e02a36c" +--- + +This is a general guide for debugging pg_cron. Below lists issues and how to debug them + +# Cannot create/edit/delete cron jobs + +Cron jobs can only be modified with the respective SQL functions: + +- Schedule: [cron.schedule](https://supabase.com/docs/guides/database/extensions/pg_cron#delete-data-every-week) +- Alter: [cron.alter_job](https://supabase.com/docs/guides/database/extensions/pg_cron#edit-a-job) +- Delete: [cron.unschedule](https://supabase.com/docs/guides/database/extensions/pg_cron#unschedule-a-job) + +If you are trying to make changes, please use the cron functions. If the cron functions are inaccessible, please contact [Support](https://supabase.com/dashboard/support/new) + +
+ +--- + +# Cron Jobs are not running + +> You should consider initiating a software upgrade in the [Infrastructure Settings](https://supabase.com/dashboard/project/_/settings/infrastructure) if your Postgres version is below v15.6.1.122. Upgrading will give you access to pg_cron v1.6.4+, which has many bug fixes and auto-revive capabilities. + +## Debugging steps: + +### Check to see if "pg_cron scheduler" is active + +pg_cron operates as the `pg_cron scheduler` process within Postgres. Use the below query to check if the worker is active + +```sql +SELECT + pid as process_id, + usename as database_user, + application_name, + backend_start as when_process_began, + wait_event_type, + state, + query, + backend_type +FROM pg_stat_activity where application_name ilike 'pg_cron scheduler'; +``` + +If the query does not return a row, the worker has died. To revive it, you must go to the [General Settings](https://supabase.com/dashboard/project/_/settings/general) and initiate a fast reboot: + +Screenshot 2024-10-29 at 12 27 24 AM + +
+ +### Check the `cron.job_run_details` table for more information + +pg_cron creates logs in its own table `cron.job_run_details"`. The below query checks for issues from the past 5 days : + +```sql +SELECT * +FROM cron.job_run_details +WHERE + (status <> 'succeeded' AND status <> 'running') + AND + start_time > NOW() - INTERVAL '5 days' +ORDER BY start_time DESC +LIMIT 10; +``` + +Respond to the errors exposed appropriately. + +> Long running jobs may show timeout errors. For jobs that are intended to execute for extended periods, consider wrapping their queries in functions with custom timeouts ([guide](https://supabase.com/docs/guides/database/postgres/timeouts#function-level)). + +
+ +### Check if there are too many cron jobs running concurrently + +pg_cron supports up to 32 concurrent jobs, each using a database connection. If too many jobs are running simultaneously, space them out to prevent connection overload and job failure. + +The below queries shows all pg_cron jobs: + +```sql +-- All jobs +select schedule, jobname, command from cron.job; + +-- Count jobs +select COUNT(*) from cron.job; +``` + +The below query can be used to find actively querying jobs: + +```sql +SELECT + pid as process_id, + usename as database_user, + application_name, + backend_start as when_process_began, + wait_event_type, + state, + query, + backend_type +FROM pg_stat_activity where application_name ilike 'pg_cron'; +``` + +You can view your peak connection usage throughout the day at the bottom of the [Reports Dashboard](https://supabase.com/dashboard/project/_/reports/database) + +
+ +### Check for database strain + +Unfortunately, excessive resource strain can slow down or disrupt jobs. + +Go to the [reports page](https://supabase.com/dashboard/project/_/reports/database) (or [Supabase Grafana](https://supabase.com/docs/guides/monitoring-troubleshooting/metrics#deploying-supabase-grafana) if you have it setup), and check for signs of resource exhaustion. If it's clear your database is under pressure, consider upgrading your compute add-on or following the advice from one of the optimization guides: + +- [Connections](https://github.com/orgs/supabase/discussions/27141) +- [Disk/IO](https://github.com/orgs/supabase/discussions/27003) +- [Memory](https://github.com/orgs/supabase/discussions/27021) +- [CPU](https://github.com/orgs/supabase/discussions/27022) + +It is important to make sure you are running the latest release of pg_cron (1.6.4) if you're noticing strain. It is the most robust. + +
+ +### Check the log explorer for more information + +Although pg*cron records errors in the `cron.job_run_details` table, in rare cases, more information can be found in the general Postgres logs. You can check the [Log Explorer](https://supabase.com/dashboard/project/*/logs/explorer) for failure events with the following query + +```sql +select + cast(postgres_logs.timestamp as datetime) as timestamp, + event_message, + parsed.error_severity, + parsed.user_name, + parsed.query, + parsed.detail, + parsed.hint, + parsed.sql_state_code, + parsed.backend_type, + parsed.application_name +from + postgres_logs + cross join unnest(metadata) as metadata + cross join unnest(metadata.parsed) as parsed +where + regexp_contains(parsed.error_severity, 'ERROR|FATAL|PANIC') + and regexp_contains(parsed.application_name, 'pg_cron') +order by timestamp desc +limit 100; +``` + +If you're interested in modifying the query, there is an advanced [guide](https://github.com/orgs/supabase/discussions/26224) for navigating the postgres logs and a general purpose [one](https://supabase.com/docs/guides/platform/advanced-log-filtering) for applying filters. + +
+ +### Create custom logs within cron jobs + +If it's still not clear what is occurring you may be able to capture more logs by running the pg_cron query inside a database function: + +```sql +create or replace function log_example() +returns void +language plpgsql +as $$ +begin + + -- Logging start of function + raise log 'logging start of cron function call: (%)', (select now()); + + -- INSERT LOGIC HERE + + -- Logging end of function + raise log 'logging end of cron function call: (%)', (select now()); + + exception + -- Handle exceptions here if needed + when others then + raise exception 'An error occurred in cron function . ERROR MESSAGE: %', sqlerrm; + +end; +$$; +``` + +You can then search for your custom messages in the [Logs Interface](https://supabase.com/dashboard/project/_/logs/postgres-logs) + +
+ +### Upgrading pg_cron version + +The current version of pg*cron on Supabase is 1.6.4. It comes with a [few bug fixes](https://github.com/citusdata/pg_cron/releases/tag/v1.6.4). You should consider upgrading to postgres v15.6.1.122+ in the[ Infrastructure Settings](https://supabase.com/dashboard/project/*/settings/infrastructure) to get the latest extension. + +
+ +### Contacting support and the maintainers + +Although Supabase includes the extension, it is maintained by Citus (a Microsoft subsidiary). You can contact Support for more help, but you should also consider creating an issue in the [pg_cron repo](https://github.com/citusdata/pg_cron). diff --git a/apps/docs/content/troubleshooting/prisma-error-management-Cm5P_o.mdx b/apps/docs/content/troubleshooting/prisma-error-management-Cm5P_o.mdx new file mode 100644 index 00000000000..520cfb19de3 --- /dev/null +++ b/apps/docs/content/troubleshooting/prisma-error-management-Cm5P_o.mdx @@ -0,0 +1,188 @@ +--- +title = "Prisma Error Management" +github_url = "https://github.com/orgs/supabase/discussions/27395" +date_created = "2024-06-19T19:51:23+00:00" +topics = [ "database" ] +keywords = [ "prisma", "timeout", "connection", "pgbouncer", "schema", "migration" ] +database_id = "28d842fd-8ba3-46f0-a307-082d946e0d94" + +[[errors]] +message = "Can't reach database server at" + +[[errors]] +message = "Timed out fetching a new connection from the connection pool" + +[[errors]] +message = "prepared statement \"\" already exists" + +[[errors]] +message = "Max client connections reached" + +[[errors]] +message = "Server has closed the connection" + +[[errors]] +message = "Drift detected: Your database schema is not in sync with your migration history" +--- + +> This guide has been deprecated. Please use the troubleshooting guide in the [Supabase docs](https://supabase.com/docs/guides/database/prisma/prisma-troubleshooting). + +# Addressing specific errors: + +Prisma, unlike other libraries, uses [query parameters for configurations](https://www.prisma.io/docs/orm/overview/databases/postgresql#arguments). + +Some can be used to address specific errors and can be appended to end of your connection string like so: + +```md +.../postgres?KEY1=VALUE&KEY2=VALUE&KEY3=VALUE +``` + +## `Can't reach database server at`: + +Increase `connect_timeout` to 30s and check to make sure you are using a valid connection string. + +```md +.../postgres?connect_timeout=30 +``` + +## `Timed out fetching a new connection from the connection pool`: + +Increase `pool_timeout` to 30s . + +```md +.../postgres?pool_timeout=30 +``` + +## `... prepared statement "" already exists` + +Add pgbouncer=true to the connection string. + +```md +.../postgres?pgbouncer=true +``` + +## `Max client connections reached` + +Checkout this [guide](https://github.com/orgs/supabase/discussions/22305) for managing this error + +## `Server has closed the connection` + +According to this [GitHub Issue for Prisma](https://github.com/prisma/prisma/discussions/7389), it may be related to large return values for queries. Try to limit the total amount of rows returned for particularly large requests. + +## `Drift detected: Your database schema is not in sync with your migration history` + +Prisma will try to act as the source of truth for your database structures. If you `CREATE`, `DROP`, or `ALTER` database objects outside of a Prisma Migration, it is likely to detect drift and may offer to correct the situation by purging your schemas. To circumvent this issue, try [baselining your migrations](https://www.prisma.io/docs/orm/prisma-migrate/workflows/baselining). + +Some users have discussed how they managed this problem in a [GitHub Discussion.](https://github.com/prisma/prisma/issues/19100#top) + +# Management suggestions + +## Make a custom role for Prisma to increase observability + +**Imagine your database as a house, and users as the people with keys.** + +- By default, most developers use the "master key" (the `postgres` role) to access everything. But it's safer to give Prisma its own key! This way, it can only access the rooms (tables) it needs. +- it's usually safer to give Prisma its own key! This way, it can only access the rooms (tables) it needs. +- Plus, with separate keys, it's easier to see what Prisma is doing in your house with monitoring tools, such as [PGAudit](https://supabase.com/docs/guides/database/extensions/pgaudit?queryGroups=database-method&database-method=sql) and [pg_stat_activity](https://supabase.com/docs/guides/platform/performance). + +### Creating the Prisma user: + +```sql +create user "prisma" with password 'secret_password' bypassrls createdb; +``` + +> Prisma requires the [createdb modifier](https://supabase.com/blog/postgres-roles-and-privileges#role-attributes) to create shadow databases. It uses them to help manage migrations. + +### Give Postgres ownership of the new user: + +This allows you to view Prisma migration changes in the [Dashboard](https://supabase.com/dashboard/project/_/editor) + +```sql +grant "prisma" to "postgres"; +``` + +### Keep it safe! + +Use a strong password for Prisma. Bitwarden provides a free and simple [password generator](https://bitwarden.com/password-generator/) that can make one for you. + +If you need to change it later, you can use the below SQL: + +```sql +alter user "prisma" with password 'new_password'; +``` + +### Grant Prisma access + +The below example gives Prisma full authority over all database objects in the public schema: + +```sql + -- Grant it necessary permissions over the relevant schemas (public) + grant usage on schema public to prisma; + grant create on schema public to prisma; + grant all on all tables in schema public to prisma; + grant all on all routines in schema public to prisma; + grant all on all sequences in schema public to prisma; + alter default privileges for role postgres in schema public grant all on tables to prisma; + alter default privileges for role postgres in schema public grant all on routines to prisma; + alter default privileges for role postgres in schema public grant all on sequences to prisma; +``` + +> For more guidance on specifying access, check out this [article](https://supabase.com/blog/postgres-roles-and-privileges#creating-objects-and-assigning-privileges) on privileges + +## Optimize Prisma queries: + +In the [Query Performance Advisor](https://supabase.com/dashboard/project/_/database/query-performance), you can view long-running or frequently accessed queries by role: + +Screenshot 2024-06-19 at 1 25 16 PM + +Selecting a query can reveal suggestions to improve its performance + +## Configuring connections + +Useful Links: + +- [How to Monitor Connections and Find the Correct Pool Size](https://github.com/orgs/supabase/discussions/27141). +- [Supavisor FAQ](https://github.com/orgs/supabase/discussions/21566) + +Supabase provides 3 connection strings in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database). You can use all three or just the ones most relevant to your project. + +### Direct connection: + +Best used with stationary servers, such as VMs and long-standing containers, but it only works in IPv6 environments unless the [IPv4 Add-On](https://supabase.com/dashboard/project/_/settings/addons) is enabled. If you are unsure if your network is IPv6 compatible, [check here](https://github.com/orgs/supabase/discussions/27034). + +```md +# Example Connection + +postgresql://postgres:[PASSWORD]@db.[PROJECT REF].supabase.co:5432/postgres +``` + +### Supavisor in session mode (port 5432): + +```md +# Example Connection + +postgres://[DB-USER].[PROJECT REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres +``` + +An alternative to direct connections when working in IPv4-only environments. + +> Session mode is a good option for migrations + +### Supavisor in transaction mode (port 6543): + +```md +# Example Connection + +postgres://[DB-USER].[PROJECT REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres +``` + +Should be used when deploying to: + +- Horizontally auto-scaling servers +- Edge/Serverless deployments + +When working in serverless/edge environments, it is recommended to set the `connection_limit=1` and then gradually increase it if necessary. diff --git a/apps/docs/content/troubleshooting/realtime-concurrent-peak-connections-quota-jdDqcp.mdx b/apps/docs/content/troubleshooting/realtime-concurrent-peak-connections-quota-jdDqcp.mdx new file mode 100644 index 00000000000..be05dc98a95 --- /dev/null +++ b/apps/docs/content/troubleshooting/realtime-concurrent-peak-connections-quota-jdDqcp.mdx @@ -0,0 +1,15 @@ +--- +title = 'Realtime "Concurrent Peak Connections" quota' +github_url = "https://github.com/orgs/supabase/discussions/19391" +date_created = "2023-12-03T17:05:20+00:00" +topics = ["realtime", "self-hosting"] +keywords = ["quota", "connections"] +--- + +The "Concurrent Peak Connections" quota refers to the maximum number of simultaneous connections to Supabase Realtime. + +For example, if you have a chat application that uses Supabase Realtime and you have 100 users who are all connected and subscribed to the chat channel, then you would have 100 concurrent peak connections. Each client can connect to multiple Realtime channels (see: https://supabase.com/docs/guides/realtime/concepts#channels). + +This quota applies to all Supabase projects, including self-hosted projects, but you can increase it depending on your use case. For hosted Supabase projects, select the plan that fits your Realtime usage and reach out if you need custom quotas. For those self-hosting Supabase, you can set those limits yourself by setting the `max_concurrent_users` field on the tenant record (see: https://supabase.com/docs/guides/self-hosting/realtime/config). + +You can learn more about Realtime quotas here: https://supabase.com/docs/guides/realtime/quotas#quotas-by-plan diff --git a/apps/docs/content/troubleshooting/resolving-42p01-relation-does-not-exist-error-W4_9-V.mdx b/apps/docs/content/troubleshooting/resolving-42p01-relation-does-not-exist-error-W4_9-V.mdx new file mode 100644 index 00000000000..48d7260e8f0 --- /dev/null +++ b/apps/docs/content/troubleshooting/resolving-42p01-relation-does-not-exist-error-W4_9-V.mdx @@ -0,0 +1,74 @@ +--- +title = "Resolving 42P01: relation does not exist error" +github_url = "https://github.com/orgs/supabase/discussions/29577" +date_created = "2024-09-29T15:52:41+00:00" +topics = [ "database" ] +keywords = [ "relation", "schema", "capitalization" ] +database_id = "72e93bc8-597d-4c72-8d86-4997679196fd" + +[[errors]] +code = "42P01" +message = "relation \"\" does not exist" + +[[errors]] +code = "42501" +message = "permission denied" +--- + +`42P01` is a[ Postgres level error](https://www.postgresql.org/docs/current/errcodes-appendix.html), that can also be found in the [PostgREST error docs](https://postgrest.org/en/v12/references/errors.html) + +```sql +42P01: relation "" does not exist +``` + +## There are a few possible causes + +--- + +### Cause 1: Search path broken + +When directly accessing a table that is not in the `public` schema, it's important to reference the external schema explicitly in your query. Below is an example from the [JS client:](https://supabase.com/docs/reference/javascript/select) + +```js +const { data, error } = await supabase.schema('myschema').from('mytable').select() +``` + +If after calling the table directly, you get a `42501` permission denied error, then you must also [expose the custom schema to the API.](https://supabase.com/docs/guides/api/using-custom-schemas). For Supabase managed schemas, such as `vault` and `auth`, these cannot be directly accessed through the DB REST API for security reasons. If necessary, they can be strictly accessed through [security definer functions.](https://supabase.com/docs/guides/database/functions?queryGroups=language&language=sql&queryGroups=example-view&example-view=sql#security-definer-vs-invoker) + +--- + +### Cause 2: Ignoring capitalization and other typos + +The table could be defined as: CREATE TABLE “Hello”`. The double quotes make it case-sensitive, so it becomes essential to call the table with the appropriate title. It is possible to change the table name to be lowercase for convenience, either in the Table Editor, or with raw SQL: + +```sql +alter table "Table_name" +rename to table_name; +``` + +--- + +### Cause 3: Table or function actually does not exist + +One may have never made the table or dropped it deliberately or accidentally. This can be quickly checked with the following query: + +```sql +-- For tables +SELECT * FROM information_schema.tables +WHERE table_name ILIKE 'example_table'; --<------ Add relevant table name +``` + +```sql +-- For functions +select + p.proname as function_name, + n.nspname as schema_name, + pg_get_functiondef(p.oid) as function_definition +from + pg_proc as p + join pg_namespace as n on p.pronamespace = n.oid +where n.nspname in ('public', 'your custom schema') -- <------ Add other relevant schemas +order by n.nspname, p.proname; +``` + +--- diff --git a/apps/docs/content/troubleshooting/resolving-500-status-authentication-errors-7bU5U8.mdx b/apps/docs/content/troubleshooting/resolving-500-status-authentication-errors-7bU5U8.mdx new file mode 100644 index 00000000000..5aec116bfc8 --- /dev/null +++ b/apps/docs/content/troubleshooting/resolving-500-status-authentication-errors-7bU5U8.mdx @@ -0,0 +1,193 @@ +--- +title = "Resolving 500 Status Authentication Errors" +github_url = "https://github.com/orgs/supabase/discussions/28652" +date_created = "2024-08-15T01:07:10+00:00" +topics = [ "auth", "database" ] +keywords = [ "authentication", "smtp", "database", "logs", "error" ] +database_id = "031ba6d7-4928-4d95-a2da-bff8dbb740ec" + +[[errors]] +http_status_code = 500 +--- + +# Resolving 500 status authentication errors + +A 500 error in Auth typically indicates an issue with an external dependency, such as your database or SMTP provider, rather than with Auth itself. This guide will help you explore the Auth logs to identify the underlying cause. + +### Prerequisites + +#### Open the log explorer + +Ensure you have access to the [Dashboard's Log Explorer](https://supabase.com/dashboard/project/_/logs/explorer) and set the time range appropriately: + +![image](/docs/img/troubleshooting/152d65ad-f0ed-47cf-8dcb-1e31c6221e71.png) + +#### Improving log readability + +Logs are displayed in a table format, which can be challenging to read. Double-clicking on a row will expand it for easier viewing: + +Screenshot 2024-08-14 at 1 15 26 PM + +## Section 1: Checking for database-level errors + +### Query for recent database errors + +Use the following SQL query to check for any recent errors the Auth server encountered while interacting with your database: + +```sql +select + cast(postgres_logs.timestamp as datetime) as timestamp, + event_message, + parsed.error_severity, + parsed.user_name, + parsed.query, + parsed.detail, + parsed.hint, + parsed.sql_state_code, + parsed.backend_type +from + postgres_logs + cross join unnest(metadata) as metadata + cross join unnest(metadata.parsed) as parsed +where + regexp_contains(parsed.error_severity, 'ERROR|FATAL|PANIC') + and regexp_contains(parsed.user_name, 'supabase_auth_admin') +order by timestamp desc +limit 100; +``` + +If no results are returned, proceed to Section 2. + +### Common database-level errors + +There are few known categories of auth/database level errors: + +### Constraint related (sql_state_code = 23503 or 23\*) + +If you’ve manually created a foreign key relationship between your tables and those in the `auth` schema, a constraint may prevent the Auth server from updating the `auth.users` table. + +#### Solution + +The log will show the name of the constraint. You need `DROP` it: + +```sql +ALTER TABLE DROP CONSTRAINT ; +``` + +Alternatively, you can `DROP` and then recreate the constraint with a less restrictive modifier, such as `SET NULL`, `SET DEFAULT`, or `CASCADE`: + +```sql +BEGIN; +ALTER TABLE DROP CONSTRAINT ; + +ALTER TABLE ADD CONSTRAINT FOREIGN KEY () + REFERENCES auth.users () + ON DELETE SET NULL; +COMMIT; +``` + +### Ownership related (sql_state_code = 42501) + +If you see an error like must be owner of..., the supabase_auth_admin role may have lost privileges over tables in the auth schema. This often results from faulty migrations by external ORMs (e.g., Prisma) or manual schema modifications. + +#### Solution + +Check ownership with this [GitHub Gist](https://gist.github.com/TheOtherBrian1/6aaaa78632b1e371f3b1c790305f0acd). If any objects are owned by the `supabase_admin` role, contact [Support](https://supabase.com/dashboard/support/new). If they're owned by roles other than `supabase_auth_admin` you can change ownership back manually one-by-one: + +```sql +ALTER OWNER TO supabase_auth_admin; +``` + +Alternatively, you can run the SQL script in this [Github Gist](https://gist.github.com/TheOtherBrian1/4714a333432b80660ff71b136b298fb8) to change all + +### Trigger related: + +If errors reference a database function, this indicates a trigger error on one of the auth tables (likely auth.users). If you do not want to keep the trigger/function, you can just quickly drop it, otherwise, continue reading to know how to fix the issue: + +```sql +-- delete the trigger with the following SQL: +DROP FUNCTION () CASCADE; + +-- If you'd prefer, you can drop the trigger alone with the following query: +-- DROP TRIGGER on auth.; +``` + +#### Solutions: + +Get the function's definition with this query: + +``` +select pg_get_functiondef(oid) +from pg_proc +where proname = ''; +``` + +##### Trigger has insufficient privileges ( sql_state_code = 42501) + +If the error is related to insufficient privileges, your trigger function is missing a security definer tag, which allows it to access schemas outside of auth. You must `REPLACE` the function with the appropriate security definer settings ([example](https://supabase.com/docs/guides/database/functions?queryGroups=language&language=js#security-definer-vs-invoker)) + +##### Trigger references a table or column that does not exist (sql_state_code = 42P01) + +The trigger may be referencing a table or column that no longer exists. In that case do one of the three: + +- make sure the trigger is referencing the table using the right schema, e.g. public, auth, etc. +- modify the trigger function to reference the appropriate database objects. +- remove the trigger +- recreate the database object that the trigger referenced + +### Corrupted schema + +If you made any customizations to the auth schema, such as adding RLS, modifying table columns, or adding/dropping tables, it can break migrations done by the Auth Server. It's necessary to remove these changes and restore the auth schema to its original form. + +## Section 2: Checking Auth level errors + +### Query for Auth errors + +Run this SQL query in the Log Explorer to find Auth-related errors: + +```sql +select + cast(metadata.timestamp as datetime) as timestamp, + msg, + event_message, + status, + path, + level +from auth_logs +cross join unnest(metadata) as metadata +where + -- find all errors + status::INT = 500 + OR + regexp_contains(level, 'error|fatal') +order by timestamp +``` + +### Database migration errors + +> "running db migrations: Migrator: problem creating schema migrations" + +This is a continuation of the "Corrupted Schema" error from the Postgres Section. If you modified structures in the auth schema, such as columns or tables, or added restrictions, such as RLS, Auth will not be able to complete it's migrations. It's necessary to remove those modifications. + +If you are running older versions of auth, you may experience a migration bug. If so, checkout this [guide](https://github.com/orgs/supabase/discussions/20722) for a resolution. If it doesn't work, please contact Support. + +### SMTP errors + +The logs may contain messages about `gomail`. It means that auth is struggling to communicate with the SMTP provider. This often implies that: + +1. Misconfigured custom domain name with your email provider. +2. Invalid email port usage. +3. Rate limiting by the SMTP provider. +4. SMTP provider downtime. + +The log will be able to provide some context for what is occurring, but it is important to check with your external SMTP provider to make sure everything is properly configured. + +## Step 3: Checking email templates + +Incomplete or incorrect email templates can also cause 500 errors. If your templates have unclosed variable tags or HTML elements, or use forbidden characters, this might be the issue. + +Test a simplified email template in the [Auth Dashboard](https://supabase.com/dashboard/project/_/auth/templates). If the new template works, revise your original template to avoid forbidden characters and ensure all elements/variables are properly closed. diff --git a/apps/docs/content/troubleshooting/resolving-database-hostname-and-managing-your-ip-address-pVlwE0.mdx b/apps/docs/content/troubleshooting/resolving-database-hostname-and-managing-your-ip-address-pVlwE0.mdx new file mode 100644 index 00000000000..b7f73e3c3fd --- /dev/null +++ b/apps/docs/content/troubleshooting/resolving-database-hostname-and-managing-your-ip-address-pVlwE0.mdx @@ -0,0 +1,39 @@ +--- +title = "Resolving database hostname and managing your IP address" +github_url = "https://github.com/orgs/supabase/discussions/22400" +date_created = "2024-04-02T09:01:54+00:00" +topics = [ "database" ] +keywords = [ "hostname", "ip", "ipv4", "ipv6" ] +database_id = "c89147f8-a66a-4c04-a1a7-45442fd7f2ee" +--- + +### Finding your database hostname + +Your database's hostname is crucial for establishing a direct connection. It resolves to the underlying IP address of your database. To find your hostname, navigate to your [Database Settings](https://supabase.com/dashboard/project/_/settings/database). It's important to note that the pooler (connection pooler) has a different IP than your database. Therefore, to reveal the database host and direct connection string, you must hide the pooler connection string. + +Example Hostname: `db.zcjtzmeifsoteyjytnbc.supabase.co` + +![Screenshot 2024-04-02 at 11 38 28 AM](/docs/img/troubleshooting/565c96e0-cbb9-4e7a-b0aa-5b423afc0ada.png) + +### Managing your IP address + +To determine your current IP address, you can use an [IP address lookup](https://whatismyipaddress.com/hostname-ip) website or the terminal command: + +- Type `nslookup hostname` and press Enter. +- This command queries the domain name servers to find the IP address of the given hostname. + +Example IPv6 Address: `2a05:d014:1c06:5f0c:d7a9:8616:bee2:30df` + +### IPv6 address + +Upon project creation, a static IPv6 address is assigned. However, it's essential to understand that this IPv6 address can change due to specific actions: + +- When a project is paused or resumed. +- During database version upgrades. + +### IPv4 address + +Opting for the static [IPv4 addon](https://supabase.com/docs/guides/platform/ipv4-address) provides a more stable connection address. The IPv4 address remains constant unless: + +- The project is paused or resumed. +- Unlike the IPv6 address, upgrading your database does not affect the IPv4 address. diff --git a/apps/docs/content/troubleshooting/rls-performance-and-best-practices-Z5Jjwv.mdx b/apps/docs/content/troubleshooting/rls-performance-and-best-practices-Z5Jjwv.mdx new file mode 100644 index 00000000000..d6f35d6a0f2 --- /dev/null +++ b/apps/docs/content/troubleshooting/rls-performance-and-best-practices-Z5Jjwv.mdx @@ -0,0 +1,235 @@ +--- +title = "RLS Performance and Best Practices" +github_url = "https://github.com/orgs/supabase/discussions/14576" +date_created = "2023-05-24T17:46:48+00:00" +topics = [ "auth", "database" ] +keywords = [ "performance", "security", "queries", "indexes", "roles" ] +database_id = "bb06ff88-e0a9-41bc-b00b-9afde10468a5" +--- + +## RLS performance and best practices + +Although most of the time spent on thinking about RLS is to get it to handle security needs, the impact of it on performance +of your queries can be massive. +This is especially true on queries that look at every row in a table like for many select operations and updates. +Note that queries that use limit and offset will usually have to query all rows to determine order, not just the limit amount so they are impacted too. + +Please see the last section for ways to measure the performance of your queries as you test RLS improvements. + +### Is RLS causing my performance issue (on a single table query)? + +For very slow queries, or if using the tools at end of article, run a query with RLS enabled on the table and then with it disabled (this should only be done in a non production environment). If the results are similar then your query itself is likely the performance issue. Although, remember any join tables in RLS will also need to run their RLS unless a security definer function is used to bypass them. You can also create a service_role client to run the query bypassing RLS if in a secure environment. + +### How to improve RLS performance. + +The following tips are very broad and each may or may not help the specific case involved. Some changes, like adding indexes, should be backed out if they do not make a difference in RLS performance and you are not using them for filtering performance. + +#### 1. The first thing to try is put an index on columns used in the RLS that are not primary keys or unique already. + +For RLS like: +`auth.uid() = user_id` +Add an index like: +`create index userid on test_table using btree (user_id) tablespace pg_default;` +Improvement seen over 100x on large tables. + +#### 2. Another method to improve performance is to wrap your RLS queries and functions in select statements. + +This method works well for jwt functions like auth.uid() and auth.jwt() as well as any other functions including security definer type. +Wrapping the function in some SQL causes an initPlan to be run by the optimizer which allows it to "cache" the results versus calling the function +on each row. +WARNING: You can only do this if the results of the query or function do not change based on the row data. +For RLS like this: +`is_admin() or auth.uid() = user_id` +Use this instead: +`(select is_admin()) OR (select auth.uid()) = user_id` + +
+ is_admin() function: + +```sql +CREATE OR REPLACE FUNCTION is_admin() + RETURNS boolean as +$$ +begin + return exists(select from rlstest_roles where auth.uid() = user_id and role = 'admin'); +end; +$$ language plpgsql security definer; +``` + +
+ +#### 3. Do not rely on RLS for filtering but only for security. + +Instead of doing this (JS client example): +`.from('table').select()` +With an RLS policy of: +`auth.uid() = user_id` +Add a filter in addition to the RLS: +`.from('table').select().eq('user_id',userId)` + +#### 4. Use security definer functions to do queries on other tables to bypass their RLS when possible. + +Instead of having this RLS where the roles_table has an RLS select policy of `auth.uid() = user_id`: +`exists (select 1 from roles_table where auth.uid() = user_id and role = 'good_role')` +Create a security definer function has_role() and do: +`(select has_role())` with code of `exists (select 1 from roles_table where auth.uid() = user_id and role = 'good_role')` +Note that you should wrap your security definer function in select if it is a fixed value per 2. +Remember functions you use in RLS can be called from the API. +Secure your functions in an alternate schema if their results would be a security leak. +Warning: If your security definer function uses row information as an input parameter be sure to test performance as you can't wrap the function as in 2. + +
+ has_role() function: + +```sql +CREATE OR REPLACE FUNCTION has_role() + RETURNS boolean as +$$ +begin + return exists (select 1 from roles_table where auth.uid() = user_id and role = 'good_role') +end; +$$ language plpgsql security definer; +``` + +
+ +#### 5. Always optimize join queries to compare row columns to fixed join data. + +Instead of querying on a row column in a join table WHERE, organize your query to get all +the column values that meet your query into an array or set. +Then use an IN or ANY operation to filter against the row column. +This RLS to allow select only for rows where the team_id is one the user has access to: +`auth.uid() in (select user_id from team_user where team_user.team_id = table.team_id)` +will be much slower than: +`team_id in (select team_id from team_user where user_id = auth.uid())` +Also consider moving the join query to a security definer function to avoid RLS on join table: +`team_id in (select user_teams())` +Note that if the `in` list gets to be over 10K items, then extra analysis is likely needed. See this follow up testing: https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/Supabase-Docs-Test . + +#### 6. Use role in TO option or roles dropdown in the dashboard. + +Never just use RLS involving auth.uid() or auth.jwt() as your way to rule out 'anon' role. +Always add 'authenticated' to the approved roles instead of nothing or public. +Although this does not improve the query performance for the signed in user it does +eliminate 'anon' users without taxing the database to process the rest of the RLS. + +### Sample results + +The code used for the below tests can be found here: [LINK](https://github.com/GaryAustin1/RLS-Perfomance): +The tests are doing selects on a 100K row table. Some have an additional join table. + +Show RLS and before after for above examples. + +| Test | RLS Before | RLS After | SQL | SQL | +| ---- | --------------------------------- | -------------------------------------------------- | ------- | ------ | +| 1 | auth.uid()=user_id | user_id indexed | 171ms | <.1 | +| 2a | auth.uid()=user_id | (select auth.uid()) = user_id | 179 | 9 | +| 2b | is*admin() \_table join* | (select is*admin()) \_table join* | 11,000 | 7 | +| 2c | is_admin() OR auth.uid()=user_id | (select is_admin()) OR (select auth.uid()=user_id) | 11,000 | 10 | +| 2d | has_role()=role | (select has_role())=role | 178,000 | 12 | +| 2e | team_id=any(user_teams()) | team_id=any(array(select user_teams())) | 173000 | 16 | +| 3 | auth.uid()=user_id | add .eq or where on user_id | 171 | 9 | +| 5 | auth.uid() in _table join on col_ | col in _table join on auth.uid()_ | 9,000 | 20 | +| 6 | No TO policy | TO authenticated (anon accessing) | 170 | <.1 | + +### Tools to measure performance + +Postgres has tools to analyze the peformance of queries. https://www.postgresql.org/docs/current/sql-explain.html +The use of explain in detail for query analysis is beyond the scope of this discussion. +Here we will use it mainly to get a performance metric to compare times. +In order to do RLS testing you need to setup the user jwt claims and change the running user to `anon` or `authenticated`. + +```sql +set session role authenticated; +set request.jwt.claims to '{"role":"authenticated", "sub":"5950b438-b07c-4012-8190-6ce79e4bd8e5"}'; + +explain analyze SELECT count(*) FROM rlstest; +set session role postgres; +``` + +This will return results like: + +``` +Seq Scan on rlstest (cost=0.00..4334.00 rows=1 width=35) (actual time=170.999..170.999 rows=0 loops=1) +" Filter: ((COALESCE(NULLIF(current_setting('request.jwt.claim.sub'::text, true), ''::text), ((NULLIF(current_setting('request.jwt.claims'::text, true), ''::text))::jsonb ->> 'sub'::text)))::uuid = user_id)" + Rows Removed by Filter: 100000 +Planning Time: 0.216 ms +Execution Time: 171.033 ms +``` + +In this case the execution time is the critical number we need to compare. + +PostgREST allows use of explain to get performance information on your queries with Supabase clients. + +Before using this feature you need to run the following command in the Dashboad SQL editor (should not be used in production): + +```sql +alter role authenticator set pgrst.db_plan_enabled to true; +NOTIFY pgrst, 'reload config'; +``` + +Then you can use the .explain() modifier to get performance metrics. + +```js +const { data, error } = await supabase + .from('projects') + .select('*') + .eq('id', 1) + .explain({ analyze: true }) + +console.log(data) +``` + +This will return a result like: + +``` +Aggregate (cost=8.18..8.20 rows=1 width=112) (actual time=0.017..0.018 rows=1 loops=1) + -> Index Scan using projects_pkey on projects (cost=0.15..8.17 rows=1 width=40) (actual time=0.012..0.012 rows=0 loops=1) + Index Cond: (id = 1) + Filter: false + Rows Removed by Filter: 1 +Planning Time: 0.092 ms +Execution Time: 0.046 ms +``` + +\*These two github discussions cover the history that lead to this analysis... + +[Stable functions do not seem to be honored in RLS in basic form](https://github.com/orgs/supabase/discussions/9311) +[current_setting can lead to bad performance when used on RLS](https://github.com/PostgREST/postgrest-docs/issues/609#) +Thanks Steve Chavez and Wolfgang Walther in those threads. + +### Added example of security definer function having select of a team table, comparing against a column in main table + +The example is in the test data as test2f. +In this case we start with a 1M row table with a team_id column. +We have a 1000 row team table that has user_ids and team(s) they belong too. +Basic RLS for a select would be `team_id = ANY(user_teams())` +This case times out with over 3 minutes as 1M rows must be searched and the function is run each time on 1000 rows. +Changing to wrap the function (method 2) `team_id = ANY(ARRAY(select user_teams()))` is a big improvement but can still take seconds. +Adding an index to team_id is the big win, but only with the second case. Without, the index case still times out. + +
+ user_teams() function returning an array: + +```sql +CREATE OR REPLACE FUNCTION user_teams() + RETURNS int[] as +$$ +begin + return array( select team_id from team_user where auth.uid() = user_id); +end; +$$ language plpgsql security definer; +``` + +
+ +Some results: + +| Policy | Index | Main Rs | Team Rs | on 10 teams | 100 | 500 | note | +| -------------------------------- | ----- | ------- | ------- | ----------- | ----- | ----- | ------------------ | +| =ANY(user_teams()) | no | 1M | 1000 | >2Min | >2Min | >2Min | TO or killed | +| =ANY(user_teams()) | yes | 1M | 1000 | >2Min | >2Min | >2Min | TO or killed | +| =ANY(ARRAY(select user_teams())) | no | 1M | 1000 | 170ms | 700 | 3300 | | +| =ANY(ARRAY(select user_teams())) | yes | 1M | 1000 | 2ms | 3 | 3 | | +| in(1,2,3...100) | no | 1M | NA | 130ms | 142 | x | baseline check | +| =ANY(ARRAY(select user_teams())) | yes | 1M | 10K | x | x | x | 24ms (on 1K teams) | diff --git a/apps/docs/content/troubleshooting/rls-simplified-BJTcS8.mdx b/apps/docs/content/troubleshooting/rls-simplified-BJTcS8.mdx new file mode 100644 index 00000000000..cda148ae2cd --- /dev/null +++ b/apps/docs/content/troubleshooting/rls-simplified-BJTcS8.mdx @@ -0,0 +1,128 @@ +--- +title = "RLS Simplified" +github_url = "https://github.com/orgs/supabase/discussions/29114" +date_created = "2024-09-05T15:20:13+00:00" +topics = [ "database", "auth" ] +keywords = [ "rls", "sql", "policy" ] +database_id = "5b8a5115-f81a-4a1f-bcc3-fdc2a2672e80" +--- + +### TL;DR: Basic summary + +Row-Level Security (RLS) Policy: A `WHERE` or `CHECK` condition applied automatically to database queries + +Key features: + +- Applies without being explicitly added to each query, which makes it good for policing row access from unknown entities, such as those leveraging the anon or authenticated roles. +- Can be set for specific actions (e.g., SELECT, INSERT) +- Can target particular database roles (e.g., "anon", "authenticated") + +Contrast with regular conditions: + +- Regular conditions: Apply to all roles and must be added manually to each query +- RLS policies: Applied automatically to specified actions and roles + +## Hands on walk-through for conditions + +### USING: + +The `USING` keyword inspects the value of row to see if it should be made visible to the query. + +When you SELECT, UPDATE, or DELETE, you have to use a WHERE statement to search for specific rows: + +```sql +-- select +select * +from some_table +where id = 5; + +-- update +update some_table +set id = 6 +where id = 5; + +-- delete +delete from some_table +where id = 6; +``` + +Even when you don't use a WHERE statement, there's still an implicit one: + +```sql +-- ...your query +WHERE true; +``` + +The `USING` clause appends more to the WHERE statement: + +```sql +-- Your Using condition +USING ( + (select auth.uid()) = user_id +); + +-- Your query without RLS: +SELECT * FROM some_table +WHERE id = 5 OR id = 6; + +-- Your query after RLS +SELECT * FROM some_table +WHERE + (id = 5 OR id = 6) + AND + (select auth.uid()) = user_id) -- <--- added by the USING clause; +``` + +### WITH CHECK: + +Let's say you have a profile table. Well, you don't want user's to be able to modify their user_id when they make an insert, do you? + +The `WITH CHECK` condition inspects values that are being added or modified. For INSERT you'd use it by itself. There's no need for a using clause: + +```sql +-- Allow users to add to table, but make sure their user_id matches the one in their JWT: + +create policy "Allow user to add posts" +on "public"."posts" +as PERMISSIVE +for INSERT +to authenticated +with check( + (select auth.uid()) = user_id +); + +-- Example: failing insert +INSERT INTO posts +VALUES (, ); + +-- Example: successful insert +INSERT INTO posts +VALUES (, ); +``` + +INSERTs do not rely on WHERE clauses, but they can have constraints. In this case, the RLS acts as a CHECK constraint against a column, e.g.: + +```sql +ALTER TABLE table_name +ADD CONSTRAINT constraint_name CHECK (condition); +``` + +What distinguishes it from normal `CHECK` constraints is that it is only activate for certain roles or methods. + +### UPDATEs: + +UPDATE both filters for rows to change and then adds new values to the table, so it requires both USING and WITH CHECK conditions: + +```sql +create policy "Allow user to edit their stuff" +on "public"."" +as RESTRICTIVE +for UPDATE +to authenticated +using ( + (select auth.uid()) = user_id +) +with check( + (select auth.uid()) = user_id +); +``` diff --git a/apps/docs/content/troubleshooting/rotating-anon-service-and-jwt-secrets-1Jq6yd.mdx b/apps/docs/content/troubleshooting/rotating-anon-service-and-jwt-secrets-1Jq6yd.mdx new file mode 100644 index 00000000000..86e2ba48a20 --- /dev/null +++ b/apps/docs/content/troubleshooting/rotating-anon-service-and-jwt-secrets-1Jq6yd.mdx @@ -0,0 +1,33 @@ +--- +title = "Rotating Anon, Service, and JWT Secrets" +github_url = "https://github.com/orgs/supabase/discussions/20031" +date_created = "2023-12-27T14:54:11+00:00" +topics = [ "auth", "platform" ] +keywords = [ "jwt", "secrets", "key", "security" ] +database_id = "caa72a34-696c-47c6-8976-e50cf7fb396e" +--- + +Has this ever been you: "Oh no! I accidentally committed my service key to our public repo!" Or maybe rotating keys is just something you regularly do for security compliance. + +Whatever the reason, here's how to rotate the keys for your Supabase project. + +1. Go to the [API Settings page](https://supabase.com/dashboard/project/_/settings/api) in the Supabase Dashboard +2. Find the JWT Secrets section + +Screenshot 2023-12-27 at 08 39 41 + +3. Click the `Generate new secret` button and choose either a random secret, or custom if you'd like to supply one of your own. +4. NOTE: Once regeneraged, all current API secrets will be immediately invalidated, and all connections using them will be severed. You will need to deploy the new secrets for connections to begin working again. +5. Confirm the changes in the warning that pops up by clicking `Generate New Secret` again. + +Screenshot 2023-12-27 at 08 39 59 + +6. After confirming, the secret will be generated, and Supabase will start rolling that out across our services. Postgres will restart, the API gateways will be updated, etc. Once the process is complete, you will be able to see your new JWT secret as well as the new anon and service keys. diff --git a/apps/docs/content/troubleshooting/security-of-anonymous-sign-ins-iOrGCL.mdx b/apps/docs/content/troubleshooting/security-of-anonymous-sign-ins-iOrGCL.mdx new file mode 100644 index 00000000000..113974ca00a --- /dev/null +++ b/apps/docs/content/troubleshooting/security-of-anonymous-sign-ins-iOrGCL.mdx @@ -0,0 +1,27 @@ +--- +title = "Security of Anonymous Sign-ins" +github_url = "https://github.com/orgs/supabase/discussions/22856" +date_created = "2024-04-18T07:45:51+00:00" +topics = [ "auth", "platform" ] +keywords = [ "anonymous", "security", "sign-in", "authentication", "identity", "policies" ] +database_id = "46bab0a2-9780-4e4f-99f5-fcc9fa51c496" +--- + +We want to clarify and provide reassurance on this topic. + +### Security overview: + +Enabling anonymous sign-ins on your project does not reduce its security. Here's why: + +- Same as Regular Users: Anonymous users function just like regular users within your project. They have unique user IDs and their own records in the authentication tables. +- Security Policies: All role-based security policies (RLS) applicable to regular users also apply to anonymous users. +- Identity Verification Measures: Even though anonymous users do not initially provide an email or phone number, the security of your project remains robust. But to prevent misuse, we recommend implementing additional security measure such as [CAPTCHA](https://supabase.com/docs/guides/auth/auth-captcha): to ensure that interactions are genuinely human. + +### Practical use cases: + +- Demo Mode: You can enable users to try out your product in a demo mode without full account creation. +- Feature Restrictions: You can limit certain actions (like posting public content) to users who sign up with more identifiable information (e.g., Google or Apple sign-ins), while still allowing anonymous users to explore your app. + +Remember, the underlying security of your project will remain the same. If your project was secure before enabling anonymous sign-ins, it will continue to be secure afterward. It's important to review and adjust your RLS policies regularly to align with your security and business needs. + +If you have further questions or need assistance in setting up, please don't hesitate to contact the [support team](https://supabase.com/dashboard/support/new). diff --git a/apps/docs/content/troubleshooting/security-of-anonymous-sign-ins-uw6_Dw.mdx b/apps/docs/content/troubleshooting/security-of-anonymous-sign-ins-uw6_Dw.mdx new file mode 100644 index 00000000000..4fdd44f80b4 --- /dev/null +++ b/apps/docs/content/troubleshooting/security-of-anonymous-sign-ins-uw6_Dw.mdx @@ -0,0 +1,47 @@ +--- +title = "Security of anonymous sign-ins" +github_url = "https://github.com/orgs/supabase/discussions/22855" +date_created = "2024-04-18T07:44:47+00:00" +topics = [ "auth", "database", "platform", "storage" ] +keywords = [ "anonymous", "security", "sign-in", "auth", "jwt" ] +database_id = "799fb49d-4c0e-4f1a-adf2-20a55a97c3c2" +--- + +A common concern is that enabling anonymous sign-ins on your project decreases its security. This is not true, and hopefully this thread will help you grasp why. + +Anonymous users are just users that don't have an email address or phone number associated yet. Otherwise, they are just like any other user in the system. They have their own row in `auth.users`, their own unique user ID, etc. + +All RLS policies in place for regular users also apply to anonymous users. Therefore, the security of the project does not decrease. To further illustrate this, let's briefly forget that anonymous users exist. + +Let's imagine a human trying to trick the project's Auth system, so they use a temp-mail service (like DuckDuckGo's duck.com or any of the multitude temp-mail services out there) to sign up. That user account has an email address, but it's not a particularly _identifying_ one. You can't link it to a human, and in fact the email address may not exist 5 minutes later. + +What I just described is the anonymous sign-ins feature, just implemented differently. + +Therefore, if the project has a security issue _before_ enabling anonymous sign-ins, they will continue to have the same security issue. If they don't have one, they will continue to not have one after enabling. + +So what's the deal then? Anonymous sign-ins (like the temp-mail case above) can be slightly easier to abuse with bots and scripts than OAuth sign-in methods. For this reason turn on CAPTCHA which ensures to some degree of certainty that it's humans and not scripts behind the sign-ins. + +Supabase Auth exposes the `is_anonymous` claim in the user's JWT if that user comes from a `supabase.auth.signInAnonymously()` call. This is mainly a _restrictive_ feature. Let's look at some examples of how it can be used. + +Suppose your project uses only Google and Apple sign-in, which to a high degree of certainty enforce "humanness." (You have to give up your address, real phone number, they track your phone etc.) But, you're faced with an adoption problem. Folks are not signing up because they want to try out the product first without giving their user data. + +One way to solve this is to add "demo mode" in your project. This will allow users to try out the real product without committing their personal data or money ahead. But this poses an issue for you -- you don't want "spam" on your platform, and you certainly don't want all features to be available without having some way to identify the human behind the actions. + +To put this in practical terms, let's imagine you're building a blogging platform. + +Users that are _quite human_ (like coming from Google or Apple) get all features. This is because you could, eventually, sue them for harassment, pornography, etc. + +But users in "demo mode" don't get all features. Maybe they can only: + +- Write blog posts as drafts, but not publish them. (Goal: Users should experience the great writing experience.) + - This can be achieved by, for example, restricting `INSERT` and `UPDATE` on the `public.posts` table when `auth.jwt()->>'is_anonymous'` is true. +- Don't allow users to attach pictures to posts. (Goal: Storage costs money, and you don't want anonymous users to upload inappropriate content.) + - Use [Storage RLS](https://supabase.com/docs/guides/storage/security/access-control) policies to restrict uploads when `is_anonymous` is true. +- Prevent anonymous users from commenting on the blogging platform, but being able to read all comments. (Goal: Keep an orderly platform.) + - This can be achieved by restricting `INSERT` on the `public.comments` table but allow `SELECT` when `is_anonymous` is true. + +Resources: + +- Postgres RLS policies (permissive vs restrictive): https://www.postgresql.org/docs/current/ddl-rowsecurity.html +- Supabase Anonymous sign-ins docs: https://supabase.com/docs/guides/auth/auth-anonymous +- Storage access control (RLS): https://supabase.com/docs/guides/storage/security/access-control diff --git a/apps/docs/content/troubleshooting/seeing-no-pghbaconf-entry-for-host-errors-in-postgres-and-they-come-from-an-ip-address-that-i-dont-recognize-4gds9f.mdx b/apps/docs/content/troubleshooting/seeing-no-pghbaconf-entry-for-host-errors-in-postgres-and-they-come-from-an-ip-address-that-i-dont-recognize-4gds9f.mdx new file mode 100644 index 00000000000..4958bf262c8 --- /dev/null +++ b/apps/docs/content/troubleshooting/seeing-no-pghbaconf-entry-for-host-errors-in-postgres-and-they-come-from-an-ip-address-that-i-dont-recognize-4gds9f.mdx @@ -0,0 +1,19 @@ +--- +title = "Seeing \"no pg_hba.conf entry for host\" errors in Postgres and they come from an IP address that I don't recognize" +github_url = "https://github.com/orgs/supabase/discussions/26282" +date_created = "2024-05-14T02:45:41+00:00" +topics = [ "database", "platform" ] +keywords = [ "pg_hba.conf", "ip", "authentication" ] +database_id = "9faf3ed3-d373-41c8-a083-ccc83dfc16d7" + +[[errors]] +message = "FATAL: no pg_hba.conf entry for host" +--- + +"FATAL: no pg_hba.conf entry for host" errors indicate that there was a failed authentication attempt to the database, so the connection couldn't be established. + +The authentication failed because the user/password credentials were invalid: `(user "xxxx", database "yyyy")`. This could happen if you're trying to connect to the database using wrong or revoked credentials. These errors indicate a failed login attempt was made to your database, meaning the connection wasn't established. + +It is common to see failed connection attempts that use default usernames (such as `user "pgbouncer"`, `database "postgres"`). Being on the public internet means some level of unauthorized access attempts are possible. These are very unsophisticated attempts that usually involve trying combinations like root, psql, test and postgres usernames. + +Supabase takes security seriously and works diligently to ensure the safety of your data. diff --git a/apps/docs/content/troubleshooting/should-i-set-a-shorter-max-age-parameter-on-the-cookies-8sbF4V.mdx b/apps/docs/content/troubleshooting/should-i-set-a-shorter-max-age-parameter-on-the-cookies-8sbF4V.mdx new file mode 100644 index 00000000000..02fcc78cc8b --- /dev/null +++ b/apps/docs/content/troubleshooting/should-i-set-a-shorter-max-age-parameter-on-the-cookies-8sbF4V.mdx @@ -0,0 +1,12 @@ +--- +title = "Should I set a shorter Max-Age parameter on the cookies?" +github_url = "https://github.com/orgs/supabase/discussions/18870" +date_created = "2023-11-10T11:53:00+00:00" +topics = [ "auth" ] +keywords = [ "cookies", "max-age", "browser", "session" ] +database_id = "e51bedf0-cdc4-4880-865b-e39c06dfe497" +--- + +The `Max-Age` or `Expires` cookie parameters only control whether the browser sends the value to the server. Since a refresh token represents the long-lived authentication session of the user on that browser, setting a short `Max-Age` or `Expires` parameter on the cookies only results in a degraded user experience. + +The only way to ensure that a user has logged out or their session has ended is to get the user's details with `getUser()`. diff --git a/apps/docs/content/troubleshooting/slow-execution-of-alter-table-on-large-table-when-changing-column-type-qmZRpZ.mdx b/apps/docs/content/troubleshooting/slow-execution-of-alter-table-on-large-table-when-changing-column-type-qmZRpZ.mdx new file mode 100644 index 00000000000..aafd894d353 --- /dev/null +++ b/apps/docs/content/troubleshooting/slow-execution-of-alter-table-on-large-table-when-changing-column-type-qmZRpZ.mdx @@ -0,0 +1,66 @@ +--- +title = "Slow Execution of ALTER TABLE on Large Table when changing column type" +github_url = "https://github.com/orgs/supabase/discussions/19747" +date_created = "2023-12-14T17:54:46+00:00" +topics = [ "database" ] +keywords = [ "alter", "table", "column", "slow", "performance" ] +database_id = "571d4ad9-b596-44bc-b8a4-a3115b862436" +--- + +If you encounter slow execution of the ALTER TABLE operation on a large table when changing a column data type, consider the following alternative approach. + +**Alternative Approach:** + +1. Add a New Column with the New Type: + `ALTER TABLE "table_name" ADD COLUMN "new_column_name" new_data_type;` + +2. Copy Values from the First Column to the Second: + `UPDATE "table_name" SET "old_column_name" = "new_column_name"::new_data_type;` + +3. Drop the Old Column: + `ALTER TABLE "table_name" DROP COLUMN "old_column_name";` + +**Why Use This Approach?** +The long execution time for the ALTER TABLE operation can be attributed to the large size of the table. This segmented approach helps in: + +- Efficiency: The process is more efficient as it avoids prolonged transactions. +- Minimizing Disruption: Migrating data systematically minimizes the impact on other operations and users. + +**Additional Recommendations:** + +- Set session statement_timeout to 0 to prevent potential transaction [timeouts](https://supabase.com/docs/guides/database/postgres/configuration). + +- Monitor currently blocked database transactions during the process using the provided script: + +``` +create view public.lock_monitor as +select + coalesce( + blockingl.relation::regclass::text, + blockingl.locktype + ) as locked_item, + now() - blockeda.query_start as waiting_duration, + [blockeda.pid](http://blockeda.pid/) as blocked_pid, + blockeda.query as blocked_query, + blockedl.mode as blocked_mode, + [blockinga.pid](http://blockinga.pid/) as blocking_pid, + blockinga.query as blocking_query, + blockingl.mode as blocking_mode +from + pg_locks blockedl + join pg_stat_activity blockeda on [blockedl.pid](http://blockedl.pid/) = [blockeda.pid](http://blockeda.pid/) + join pg_locks blockingl on ( + blockingl.transactionid = blockedl.transactionid + or blockingl.relation = blockedl.relation + and blockingl.locktype = blockedl.locktype + ) + and [blockedl.pid](http://blockedl.pid/) <> [blockingl.pid](http://blockingl.pid/) + join pg_stat_activity blockinga on [blockingl.pid](http://blockingl.pid/) = [blockinga.pid](http://blockinga.pid/) + and blockinga.datid = blockeda.datid +where + not blockedl.granted + and blockinga.datname = current_database(); +``` + +**Notes:** +Execute these steps during a maintenance window or low-traffic period to minimize disruption. diff --git a/apps/docs/content/troubleshooting/steps-to-improve-query-performance-with-indexes-q8PoC9.mdx b/apps/docs/content/troubleshooting/steps-to-improve-query-performance-with-indexes-q8PoC9.mdx new file mode 100644 index 00000000000..0581919d013 --- /dev/null +++ b/apps/docs/content/troubleshooting/steps-to-improve-query-performance-with-indexes-q8PoC9.mdx @@ -0,0 +1,82 @@ +--- +title = "Steps to improve query performance with indexes" +github_url = "https://github.com/orgs/supabase/discussions/22449" +date_created = "2024-04-03T20:10:02+00:00" +topics = [ "database", "platform", "cli" ] +keywords = [ "indexes", "performance", "optimization", "grafana", "query" ] +database_id = "fb1cbd42-e172-44b2-af2b-fda5aecde5c2" + +[api] +cli = [ "supabase-inspect-db" ] +--- + +# Optimizing your database + +This is an intermediate and actionable guide for PostgreSQL optimization within the Supabase ecosystem. + +> Consider checking out [Index_advisor](https://supabase.com/docs/guides/database/extensions/index_advisor) and the [performance advisor](https://supabase.com/dashboard/project/_/database/performance-advisor) now available in the Dashboard! + +## Installing Supabase Grafana + +Supabase has an [open-source Grafana Repo](https://github.com/supabase/supabase-grafana) that displays real-time metrics of your database. Although the [Reports Dashboard ](https://supabase.com/dashboard/project/_/reports) provides similar metrics, it averages the data by the hour or day. Having visibility over how your database responds to changes helps to ensure that the database is not stressed by the index-building process. + +_Visual of Grafana Dashboard_ +![image](/docs/img/troubleshooting/18ed2c88-332e-4e66-b9b4-c37e99a39104.png) + +It can be run locally within Docker or can be deployed for free to fly.io. Installation instructions can be found in [Supabase's metrics docs](https://supabase.com/docs/guides/platform/metrics#deploying-supabase-grafana) + +## Query optimization through indexes + +Disk (storage) is relatively slow compared to memory, so Postgres will take frequently accessed data and cache it in memory for fast access. + +Ideally, you want the cache hit rate (cache-hits/total-hits) to be 99%. You should try to run the following query on your instance: + +```sql +select + 'index hit rate' as name, + (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) as ratio +from pg_statio_user_indexes +union all +select + 'table hit rate' as name, + sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) as ratio +from pg_statio_user_tables; +``` + +If the cache hit rate is relatively low, it often means that you need to increase your memory capacity. The second metric that is often inspected is index usage. Indexes are data structures that allow Postgres to search for information quickly - think of them like you would think of an index at the back of a book. Instead of scanning every page (or row), you can use an index to find the contents you need quickly. For a better understanding of how Postgres decides on whether to use an index or not, check out this [explainer](https://github.com/orgs/supabase/discussions/26959). + +The index hit rate (how often an index is used) can usually be improved moderately. + +There's a query to find out how often an index is used when accessing a table: + +```sql +select + relname, + 100 * idx_scan / (seq_scan + idx_scan) as percent_of_times_index_used, + n_live_tup as rows_in_table +from pg_stat_user_tables +where seq_scan + idx_scan > 0 +order by n_live_tup desc; +``` + +A lot of the [queries for inspecting performance](https://supabase.com/docs/reference/cli/supabase-inspect-db) are actually pre-bundled as part of the [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started). For instance, there is a command for testing which indexes of yours are unnecessary and are needlessly taking up space: + +```bash +npx supabase login + +npx supabase link + +npx supabase inspect db unused-indexes +``` + +There is an extension called [index_advisor](https://supabase.com/docs/guides/database/extensions/index_advisor) that creates virtual indexes on your queries and then checks which ones increase performance the best. Unlike normal index creation, virtual indexes can be made rapidly, which makes uncovering the most performant solutions fast. The [Query Performance Advisor](https://supabase.com/dashboard/project/_/database/query-performance) in the Dashboard is configured to use index_advisor to make optimization suggestions and you should check it out to see where you can improve. + +Index_advisor won't test indexes added through extensions nor will it test GIN/GIST indexes. For JSON or ARRAY columns, consider exploring GIN/GIST indexes separately from index_advisor. If you're using pg_vector, it's crucial to use an [HSNW index](https://github.com/orgs/supabase/discussions/21379). + +Indexes can significantly speed up reads, sometimes boosting performance by 100 times. However, they come with a trade-off: they need to track all column changes, which can slow down data-modifying queries like UPDATEs, DELETEs, and INSERTs. + +Generally, indexes offer more benefits. For example, primary key columns automatically have a B-Tree index, enhancing read and join operations without significantly affecting write queries. Nonetheless, it's wise to avoid carelessly adding indexes. + +Some indexes may take a long time to build. A [guide](https://github.com/orgs/supabase/discussions/21379) was written for applying HSNW indexes, but it can be generalized and referenced for applying others, too. + +When building an index, the affected table is locked, preventing write operations. If this poses an issue, use the [`CONCURRENTLY` modifier](https://www.postgresql.org/docs/current/sql-createindex.html). However, reserve this for necessary cases only, as it entails building the index twice, prolonging the process and increasing computational costs. diff --git a/apps/docs/content/troubleshooting/supabase--your-network-ipv4-and-ipv6-compatibility-cHe3BP.mdx b/apps/docs/content/troubleshooting/supabase--your-network-ipv4-and-ipv6-compatibility-cHe3BP.mdx new file mode 100644 index 00000000000..07e1940817b --- /dev/null +++ b/apps/docs/content/troubleshooting/supabase--your-network-ipv4-and-ipv6-compatibility-cHe3BP.mdx @@ -0,0 +1,83 @@ +--- +title = "Supabase & Your Network: IPv4 and IPv6 compatibility" +github_url = "https://github.com/orgs/supabase/discussions/27034" +date_created = "2024-06-05T16:26:47+00:00" +topics = [ "database", "platform", "supavisor" ] +keywords = [ "ipv4", "ipv6", "network", "compatibility", "address" ] +database_id = "f27145c7-0ff5-4621-a364-5d5704bce0ff" +--- + +# Network compatibility with your Supabase database + +The internet uses a system called the Internet Protocol (IP) to route communication between devices. There are two main versions: + +- **IPv4**: Introduced in 1980, it's the original version. +- **IPv6**: Launched in 1999, it offers a much larger address space and is the preferred future-proof option. + +### Supabase and IPv6: + +All Supabase databases provide a direct connection string that maps to an IPv6 address. + +### Working with IPv6 incompatible hosts: + +Here are your options if your server platform doesn't support IPv6: + +- Use the Supavisor Connection String (available in the [Dashboard](https://supabase.com/dashboard/project/_/settings/database)). +- Use the [Supabase Client libraries](https://supabase.com/docs/guides/api/rest/client-libs), which are IPv4 compatible. +- Enable the [dedicated IPv4 Add-On](https://supabase.com/dashboard/project/_/settings/addons) (available to Pro and above orgs) + +> Note: the IPv4 Add-On costs $0.0055 an hour, which equates to ~$4.00 if left on for a full month (~720 hours) + +### Checking IPv6 support: + +The majority of services are IPv6 compatible. However, there are a few prominent ones that only accept IPv4 connections: + +- [Retool](https://retool.com/) +- [Vercel](https://vercel.com/) +- [GitHub Actions](https://docs.github.com/en/actions) +- [Render](https://render.com/) + +If you're still unsure if your network supports IPv6, you can run this cURL command on your deployment server: + +```sh +curl -6 https://ifconfig.co/ip +``` + +If the command returns an IPv6 address, the network is IPv6 compatible. + +### Finding your database's IP address: + +To determine your current IP address, you can use an IP address [lookup website](https://whatismyipaddress.com/hostname-ip) or the terminal command: + +```sh +nslookup db..supabase.co +``` + +This command queries the domain name servers to find the IP address of the given hostname. + +Example IPv6 Address: `2a05:d014:1c06:5f0c:d7a9:8616:bee2:30df` + +### Identifying your connections: + +The pooler and direct connection strings can be found in the [database settings](https://supabase.com/dashboard/project/_/settings/database): + +> "Note uses an IPv6 address by default + +```sh +# Example connection string +postgresql://postgres:[YOUR-PASSWORD]@db.ajrbwkcuthywfihaarmflo.supabase.co:5432/postgres +``` + +#### Supavisor in transaction mode (port 6543) + +```sh +# Example transaction string +postgresql://postgres.ajrbwkcuthywddfihrmflo:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres +``` + +#### Supavisor in session mode (port 5432) + +```sh +# Example session string +postgresql://postgres.ajrbwkcuthywfddihrmflo:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres +``` diff --git a/apps/docs/content/troubleshooting/supabase-dashboard-not-loading-project-not-loading-on-dashboard-LfMq9F.mdx b/apps/docs/content/troubleshooting/supabase-dashboard-not-loading-project-not-loading-on-dashboard-LfMq9F.mdx new file mode 100644 index 00000000000..8f30a43a3e0 --- /dev/null +++ b/apps/docs/content/troubleshooting/supabase-dashboard-not-loading-project-not-loading-on-dashboard-LfMq9F.mdx @@ -0,0 +1,13 @@ +--- +title = "Supabase dashboard not loading/ Project not loading on dashboard" +github_url = "https://github.com/orgs/supabase/discussions/14172" +date_created = "2023-05-06T10:01:16+00:00" +topics = [ "studio" ] +database_id = "9d3377c9-fa79-4734-9fa9-8ccc99415f7f" +--- + +We suggest you try clearing your browser's cookies, cache and see if that helps. + +To isolate whether this issue is specific to your browser, we recommend updating your browser to the latest version as it may help resolve the issue. Also, we recommend you try accessing the dashboard from a different browser or incognito mode/private browsing mode. + +If you still see the same issue, please write to [support@supabase.com](mailto:support@supabase.com) using your email. diff --git a/apps/docs/content/troubleshooting/supavisor-and-connection-terminology-explained-9pr_ZO.mdx b/apps/docs/content/troubleshooting/supavisor-and-connection-terminology-explained-9pr_ZO.mdx new file mode 100644 index 00000000000..f9f871d09ea --- /dev/null +++ b/apps/docs/content/troubleshooting/supavisor-and-connection-terminology-explained-9pr_ZO.mdx @@ -0,0 +1,72 @@ +--- +title = "Supavisor and Connection Terminology Explained" +github_url = "https://github.com/orgs/supabase/discussions/27388" +date_created = "2024-06-19T15:56:26+00:00" +topics = [ "supavisor", "database" ] +keywords = [ "connections", "pooler", "transaction", "session", "pgbouncer" ] +database_id = "e008e056-453d-44b5-8a8a-27c2d70681fe" +--- + +I'll be the first to admit that the official naming conventions in the Postgres community can be a bit confusing, so here's a basic rundown. It's a bit long, so feel free to just jump to the portion relevant to you: + +## Clients: + +This is any server trying to connect to the pooler or database. This can be confusing because the term client in other scenarios also means users connecting through a frontend. + +## Client connections: + +A single application server can create multiple connections. A client connection represents any connection established by a server to the pooler. + +## Direct/database connections: + +This represents a direct connection to the database. + +Your application servers can directly connect using the db connection string: + +```md +postgresql://postgres:[PASSWORD]@db.[PROJECT REF].supabase.co:5432/postgres +``` + +When connecting through the pooler, it will establish db/direct connections on behalf of clients. It then forwards/triages requests from client connections to db/direct connections. + +## Max_connections: + +Configured with the max_connections system variable, it represents how many direct/database connections postgres will tolerate. You can view your instance's settings by running this SQL: + +```sql +SHOW max_connections; +``` + +To know more about configuring this value, check out [monitoring connections troubleshooting guide.](https://github.com/orgs/supabase/discussions/27197) + +## Pool size: + +The number of direct connections Supavisor is permitted to request from the database to manage each [database/role/mode combination](https://github.com/orgs/supabase/discussions/21566). + +## Transaction mode: + +Transaction mode gives the pooler permission to share direct connections among multiple clients. It is used when the pooler connection string is listening on port 6543: + +```md +#example transaction mode string +postgres://postgres.obfwhevidiamwdwki:[YPASSWORD]@aws-0-ca-central-1.pooler.supabase.com:**6543**/postgres +``` + +Postgres connections use the Postgres Wire Protocol (PWP) rather than HTTP. PWP acts like a websocket: once a connection is made, it stays open and active until the client disconnects. + +If too many connections are held by idle or greedy clients, other application servers won’t be able to connect to your database. Transaction mode helps avoid this problem by allowing clients to access the database connections only when they are running a query. This reduces the chances of hitting the maximum direct connection limit. + +## Session mode: + +session mode restricts the pooler, forcing it to grant an underlying direct connection exclusively to a single client connection. + +```md +#example session mode string | uses port 5432 +postgres://postgres.obfwhevidiamwdwki:[YPASSWORD]@aws-0-ca-central-1.pooler.supabase.com:**5432**/postgres +``` + +Session mode behaves nearly identically to a standard direct connection, which makes one wonder: what is the point? + +One benefit is that if you need long-lasting connections in an IPv4-only environment and do not want to enable the IPv4 Add-On, session mode resolves this issue. + +Another clear benefit is queueing. If all direct connections were taken, without the pooler, the n+1 client that tried to connect would be rejected. It would have to constantly poll the database to see if a slot became available. The pooler in session mode will queue a client for up to a minute, so it doesn't have to constantly poll while waiting for a slot. diff --git a/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx b/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx new file mode 100644 index 00000000000..cbdb3e92ead --- /dev/null +++ b/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx @@ -0,0 +1,180 @@ +--- +title = "Supavisor FAQ" +github_url = "https://github.com/orgs/supabase/discussions/21566" +date_created = "2024-02-26T16:23:40+00:00" +topics = [ "supavisor", "database" ] +keywords = [ "pooler", "connections", "supavisor", "database" ] +database_id = "f252ba39-e540-462b-868f-42eb31a5d40c" +--- + +### What problems do poolers solve? + +PostgreSQL stands out from other databases by opting to create a new process, not a new thread, for each direct connection. While this design choice brings [numerous benefits](https://www.postgresql.org/message-id/1098894087.31930.62.camel@localhost.localdomain), it introduces a startup penalty for new connections. Moreover, connections are more memory-intensive and can strain Postgres's internal schedulers, limiting the sustainable number that can be formed. Resultingly, developers must be mindful of how they allocate the resource. + +When a client (backend server) connects to PostgreSQL, the connection is stateful and enduring, allowing clients to greedily hold onto connections without any obligation to give them up. Typically, applications do not continuously send queries to a database, so this pattern underutilizes finite connections that could have serviced other clients. + +PostgreSQL's shortcomings are particularly evident when handling transient servers, like edge functions. They not only hoard connections for brief queries but also aggressively open and close connections, straining the database. + +### How do poolers solve the problem? + +A pooler is ultimately a load balancer for database connections. It maintains several hot connections that it triages to clients. This reduces the startup cost of creating a new process on Postgres. The pooler can also more efficiently manage a database's finite connections by only allowing clients to access them when they need to execute a query (A.K.A. transaction mode). + +### Are poolers necessary? + +All database connection libraries, such as Prisma, SQLAlchemy, and Postgres.js have built-in poolers. These are known as application-side poolers and they are fundamental for sustainable connection management. Most libraries have default pool sizes that may need to be changed for specific workloads. As an example, most edge/serverless functions are called to service a single user's request. They usually require significantly fewer connections (often 1 is optimal) than a dedicated application server. + +Note, that like databases, servers can only maintain a certain amount of connections themselves. If you were to increase the number aggressively, the server may not be able gracefully orchestrate them. For instance, Prisma's [default pool size](https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/databases-connections/connection-pool#connection-pool-size) is automatically set to one more than twice the server's CPU count (1 + 2 \* num_of_CPUs). The Prisma Team chose this value because it is generally performant with ORM's internal architecture. + +When deploying to static architecture, such as long-standing containers or VMs, application-side poolers are satisfactory on their own. + +When connecting to your application from serverless/edge functions, horizontally auto-scaling servers, or in cases where you simply need more connections than what the database can manage, it's best to complement application-side poolers with a serverside one. Supabase provides Supavisor as an option, but you could use alternatives, such as Prisma's Accelerate or Cloudflare's Hyperdrive. + +They sit between the database and your client servers. They are solely optimized for sustaining high numbers of client connections and queuing and triaging queries to the database. Although they add network complexity, they are necessary when managing auto-scaling servers that can hypothetically form an infinite amount of connections. + +### Where are the connection strings + +Supabase provides 3 database connection strings that can be used simultaneously if necessary. All can be viewed in the connection string section of the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) + +Screenshot 2024-07-04 at 10 40 38 AM + +#### Direct connections: + +> "Note uses an IPv6 address by default. [Check here to see if your network is IPv6 compatible](https://github.com/orgs/supabase/discussions/27034)" + +```sh +# Example connection string +postgresql://postgres:[YOUR-PASSWORD]@db.ajrbwkcuthywfihaarmflo.supabase.co:5432/postgres +``` + +#### Supavisor in transaction mode (port 6543) + +```sh +# Example transaction string +postgresql://postgres.ajrbwkcuthywddfihrmflo:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres +``` + +#### Supavisor in session mode (port 5432) + +```sh +# Example session string +postgresql://postgres.ajrbwkcuthywfddihrmflo:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres +``` + +### Supavisor: Transaction mode vs. Session mode? + +When a client forms a direct connection with PostgreSQL, it usually will make a few queries, but may not utilize the connection the entire time. In transaction mode, a client is allowed to make a single query before being sent back to the figurative "waiting room". This prevents greedy or sedentary clients from hoarding connections. In most cases, this increases query throughput and is optimal. + +In session mode, once the pooler assigns a direct connection, it stays with that client until voluntarily surrendered. + +This behavior mirrors a direct connection, allowing greedy clients to monopolize the pool. This raises a question: what is the purpose of session mode? + +Depending on your application's configurations, having the pooler manage a queue of patient clients is preferable to the alternative of constantly polling the database to check for an available connection. Session mode can queue clients for up to a minute. If this isn't particularly relevant to your application design, then the primary benefit is that it is [IPv4 compatible](https://github.com/orgs/supabase/discussions/27034). Also, unlike transaction mode, it supports prepared statements. + +### What happens when a client library, such as prisma, connects through Supavisor? + +When clients connect to either PostgreSQL or Supavisor, they do so with the PostgreSQL Wire Protocol. Because of this, clients treat connections with pooler as if they were directly connected to PostgreSQL. The pooler then smoothly acts as a messenger between the database and the client. + +### What are "client connections"? + +TL;DR: They have nothing to do with front-end clients. They are the amount of backend-server connections that can connect to a serverside pooler. + +Imagine a chess tournament with 60 boards. Each board represents a connection in a database. When a player sits down at a board, it's like a client connecting to the database. They can take their time with their moves or just sit there, not making any. + +But when the tournament fills up and all the boards are taken, new players are turned away, and told to check back at a later time to see if a table becomes available. + +Now, imagine the tournament organizers decide to expand the venue to house 200 people without adding more tables. Even when all boards are occupied, players don't have to leave. They can wait in the wings, and the moment a board opens up, someone from the waiting area can take their place. Likewise, if someone ends their game, but wants to play again, they can just go back to the waiting area. The waiting area represents the "Max Client Connections". Ultimately, the additional capacity provided by the pooler ensures fewer people are turned away from the "tournament". + +### In the context of Supavisor, what does "pool size" mean? + +"Pool size" refers to the maximum number of direct connections the pooler can maintain per unique user, database, and mode combination. You can adjust it in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) to strike a balance between efficient resource utilization and accommodating peak traffic. + +### What is the "user+db+mode" combination? + +PostgreSQL is not actually a database. It is a Relational Database Management System (RDMS). Within it, you can spawn PostgreSQL databases. In Supabase, it is a common pattern to just use the default database called postgres, but you could create more: + +```sql +CREATE DATABASE postgres; +CREATE DATABASE another_database; +``` + +Similarly, a database can have many database users, but most people just rely on the default user "postgres". + +```sql +CREATE USER postgres WITH PASSWORD 'super-secret-password; +CREATE USER some_new_user WITH PASSWORD 'password'; +``` + +The modes are transaction (port 6543) and session (port 5432) mode. + +The "user+database+mode" combinations are formed from the above variables and are used within the connection string: + +``` +postgres://[USER].shfmmplnqscentnakbkl:[password]@aws-0-ca-central-1.pooler.supabase.com:[MODE]/[DATABASE] +``` + +When a distinct combination connects to your database, a new direct connection pool will be created. That means if you have two combinations connecting and the "Pool Size" is set to 120, each combination will have permission to form 120 connections. This can become problematic if collectively they exhaust all available direct connections. + +### **Does Supavisor immediately establish the max pool size?** + +No, it doesn't. Let's break it down: + +- In transaction mode, the pooler will only add connections if there are not enough connections to service all pending queries simultaneously. If 10 clients connect, but a single direct connection can comfortably accommodate them all, the pooler will not create more. +- If necessary, it can create as many connections to prevent queries from pending up to the limit specified by the "Pool Size". +- In session mode, it will immediately create a new a direct connection for a new client unless the "Pool Size" is reached +- In transaction mode, once a direct connection is established, the pooler will keep it available for reuse for another client. However, if the connection remains unused for 5 minutes, the pooler will close it to free up database resources. In session mode, the connection will be immediately closed. +- If a client is connected to the pooler, then at least 1 hot connection will be sustained, even if no client needs it. + +### **Where can I change my pool size?** + +In the [Dashboard's Database Settings](https://supabase.com/dashboard/project/_/settings/database), you can configure Supavisor's "Pool Size": + +Screenshot 2024-02-25 at 10 06 25 PM + +You can also change the pool size for PostgREST's (DB API) internal pooler at the bottom of the [application settings](https://supabase.com/dashboard/project/_/settings/api). + +### Do all services on Supabase use Supavisor? + +Supabase Storage uses Supavisor internally. The other servers that communicate with Postgres (PostgREST, Realtime, and Auth) all rely on internal application poolers. + +Supavisor is primarily intended for users who do not want to rely on the Supase Client libraries and instead prefer to work with external ORMs, such as Prisma, Drizzle, and Psycopg. + +### **Should I change Supavisor's pool size?** + +In an ideal scenario, PostgreSQL would support an unlimited number of direct connections, but there's a limit to how many it can handle. If Supavisor uses most of the available connections, you risk depriving other servers, such as Auth, from accessing your database. Still, as much as possible, you want to give your pooler freedom to grow its pool as needed to service demand. + +It's important to note that the Storage server also uses Supavisor as a unique "user+db+mode" combination, so the pool size you set for your general application will apply to it, too. + +As a rule of thumb, if you're using the DB REST API or multiple app-based "user+db+mode" combinations, try to keep the pooler's usage under 40% of available connections. Otherwise, you can cautiously increase usage to around 80%. These percentages are flexible and depend on your application's usage and setup. Monitor connection usage to determine the optimal allocation without depriving other servers of necessary connections. + +### **How can I monitor connections?** + +> EDIT: a more in-depth [troubleshooting guide](https://github.com/orgs/supabase/discussions/27141) for connection monitoring was published + +Connection usage can be monitored with a Supabase Grafana Dashboard. It provides realtime visibility of over 200 database metrics, such as graphs of CPU, EBS, and active direct/pooler connections. It can be extremely useful for monitoring and debugging instances. + +You can check our [GitHub repo](https://github.com/supabase/supabase-grafana) for setup instructions for local deployments or free cloud deployments on [Fly.io](http://fly.io/). I recommend referring to our concise [documentation](https://supabase.com/docs/guides/platform/metrics) to learn more about the metrics endpoint. + +### **Can Supavisor really support a million connections?** + +It depends. + +In the article ["Supavisor: Scaling Postgres to 1 Million Connections"](https://supabase.com/blog/supavisor-1-million) we gave Supavisor the capacity to connect to a million clients simultaneously. The pooler triaged the clients' queries to 400 database connections. + +What is important is that the database processing these queries had 64vCPUs and 256GB of memory. It could process the queries fast enough to ensure that there was never a noticeable bottleneck. The clients never waited long enough to timeout. If the clients were asking to run inefficient queries, like the one below, a severe backlog of pending client queries would have formed: + +```sql +-- do nothing for 60 seconds +select pg_sleep(60); +``` + +A backlog could also happen if there are not enough direct connections available. In our setup, 400 connections were enough to handle a million clients, ensuring a smooth flow of requests. But if we had only 1 direct connection, the queue would've moved too slowly and clients would expire their requests. + +Scaling to this level only works when the pooler is operating in transaction mode. In Session mode, hypothetically, the 400 clients that first accessed the direct connections could keep them, even if they are no longer executing queries. The pending 999,600 clients waiting for a turn would then be starved of direct connections. diff --git a/apps/docs/content/troubleshooting/transferring-from-cloud-to-self-host-in-supabase-2oWNvW.mdx b/apps/docs/content/troubleshooting/transferring-from-cloud-to-self-host-in-supabase-2oWNvW.mdx new file mode 100644 index 00000000000..0d682cc00bf --- /dev/null +++ b/apps/docs/content/troubleshooting/transferring-from-cloud-to-self-host-in-supabase-2oWNvW.mdx @@ -0,0 +1,16 @@ +--- +title = "Transferring from cloud to self-host in Supabase" +github_url = "https://github.com/orgs/supabase/discussions/22712" +date_created = "2024-04-14T15:19:10+00:00" +topics = [ "database", "self-hosting" ] +keywords = [ "migrate", "pg_dump", "psql", "self-host" ] +database_id = "c6b6ae3c-1b5b-4ba8-a40f-5f2ca1f007e2" +--- + +To migrate from cloud to self-hosting, you can use the [pg_dump](https://www.postgresql.org/docs/9.6/app-pgdump.html) command to export your database to an SQL file, which then you can run on any database to load the same data in. + +You can then try to import your SQL files using psql from the terminal: + +`psql -h 127.0.0.1 -p 5432 -d postgres -U postgres -f .sql` + +You can also find some useful information about self-hosting here: https://supabase.com/docs/guides/self-hosting. diff --git a/apps/docs/content/troubleshooting/understanding-postgresql-explain-output-Un9dqX.mdx b/apps/docs/content/troubleshooting/understanding-postgresql-explain-output-Un9dqX.mdx new file mode 100644 index 00000000000..102a13c34dd --- /dev/null +++ b/apps/docs/content/troubleshooting/understanding-postgresql-explain-output-Un9dqX.mdx @@ -0,0 +1,210 @@ +--- +title = "Understanding PostgreSQL EXPLAIN Output" +github_url = "https://github.com/orgs/supabase/discussions/22839" +date_created = "2024-04-17T16:31:47+00:00" +topics = [ "database" ] +keywords = [ "explain", "performance", "query" ] +database_id = "0ce5b1e4-bd0a-439e-9fb1-8e27bad2ef10" + +[api] +sdk = [ "explain" ] +--- + +### Introduction + +This guide is designed to help you understand how to use the PostgreSQL [EXPLAIN and EXPLAIN ANALYZE](https://www.postgresql.org/docs/current/sql-explain.html) commands to optimize and debug SQL queries. Understanding the output of these commands can help you improve the performance of your applications by optimizing database interactions. + +### What is explain? + +The PostgreSQL EXPLAIN command shows the execution plan of a SQL query. This plan describes how the PostgreSQL database will execute the query, including how tables will be scanned—by using sequential scans, index scans, etc.—and how rows will be joined. + +### How to use explain in Supabase + +**Using EXPLAIN through the SQL Editor** + +1. Access your Supabase project +2. Open the SQL editor +3. Execute EXPLAIN with Your Query: + `EXPLAIN SELECT * FROM users WHERE user_id = 1;` + +**Using EXPLAIN with supabase-js Library** + +1. Follow the [Performance Debugging Guide](https://supabase.com/docs/guides/database/debugging-performance) to enable the functionality on your project. +2. Once debugging is enabled, you can use the [EXPLAIN](https://supabase.com/docs/reference/javascript/explain?queryGroups=example&example=get-execution-plan-with-analyze-and-verbose) function in your application code. Here's how to use it + +``` +const { data, error } = await supabase + .from('countries') + .select() + .explain({analyze:true,verbose:true}) +``` + +### Detailed breakdown of explain output components + +### 1. Plan type + +**- Seq Scan:** A sequential scan that reads all rows from a table. This is often seen in the absence of indexes that can be used for the query. + +**- Index Scan:** Uses an index to find rows quickly. This indicates that the query is able to use an index to efficiently locate data. + +**- Bitmap Heap Scan:** Uses a bitmap index to find rows quickly and then retrieves the actual rows from the table. This type of scan is efficient when retrieving a moderate number of rows. + +### 2. Cost + +**cost=0.00..19.00:** This represents the estimated cost of executing the plan. The first number (0.00) is the cost of returning the first row, and the second number (19.00) is the total cost of executing the query and retrieving all rows. These values are arbitrary units determined by the database's cost model, not actual time or resources. + +**Start-up Cost:** The cost incurred before returning the first row. +**Total Cost:** The estimated total cost of the operation. + +### 3. Rows + +**rows=1:** An estimate of the number of rows the query will return. Accurate row estimates depend on updated statistics; if the estimates are off, it might indicate that the database statistics need to be updated. + +### 4. Width + +**width=240:** The average number of bytes expected for the output rows. This indicates the expected data volume that needs to be processed or transferred, which can impact performance. + +### 5. Filters + +**Filter: (user_id = 1):** This shows any filters applied post-scan. Filters are conditions that are checked after retrieving the rows. A high number of rows being filtered out could indicate a need for better indexing. + +### 6. Execution time + +**Execution Time: 0.069 ms:** measures how long it took to actually execute the query, including retrieving the data, performing any sorts, joins, or other operations defined in the execution plan, and returning the final results. This time is measured in milliseconds. + +![image](/docs/img/troubleshooting/10950be3-264b-4a41-bc71-3d431c2756b2.png) + +### Detailed components in explain analyze + +When running EXPLAIN ANALYZE, additional information is provided, including: + +- **Actual time:** Shows the time actually spent executing the scan and retrieving rows. This is split into the time to retrieve the first row (first) and the time to retrieve all rows (last). + +- **Rows removed by filter:** Indicates how many rows were excluded due to not meeting the filter conditions. + +- **Loops:** Shows how many times the node was executed, especially relevant in nested loop joins or subqueries. + +Example of Detailed EXPLAIN ANALYZE Output + +``` +Seq Scan on users (cost=0.00..19.00 rows=1 width=240) (actual time=0.026..0.026 rows=1 loops=1) + Filter: (user_id = 1) + Rows Removed by Filter: 999 +Planning Time: 0.135 ms +``` + +- **actual time=0.026..0.026:** The time it took to start returning rows and to finish returning all rows. + +- **Rows Removed by Filter:** 999: Indicates that many rows were checked against the filter, but most did not match the criteria. + +- **Planning Time: 0.135 ms:** Planning Time refers to the amount of time the PostgreSQL query planner takes to analyze the query and create an execution plan. This time is measured in milliseconds. + +### You might be asking yourself now, why there is two different sets of metrics : `(cost=0.42..2.64 rows=1 width=164) (actual time=0.020..0.021 rows=1 loops=1)`? + +To answer you, one is for the estimated cost and performance, and another for the actual performance of the query as explained above. + +**Planning vs. Execution**: The estimates are based on the query planner's understanding of the data (gathered from statistics about table size, distribution of values, etc.), while the actual metrics tell you what really happened when the query ran. This comparison can highlight inaccuracies in the planner's assumptions. + +**Performance Tuning**: By comparing estimated rows and actual rows, or estimated time and actual time, you can identify potential performance issues. For example, if the estimated rows are significantly off from the actual rows, it might suggest that the table statistics are outdated, leading to inefficient query plans. + +**Identifying Bottlenecks**: If the actual time is significantly higher than expected, or if loops are more frequent than anticipated, these could be indicators of performance bottlenecks in the query. + +### How to read a complex explain output + +First, you have to understand that a PostgreSQL execution plan is a tree structure consisting of several nodes. The top node (the Aggregate above) is at the top, and lower nodes are indented and start with an arrow (->). Nodes with the same indentation are on the same level (for example, the two relations combined with a join). + +Here's an example: + +``` +Aggregate Node (Top Node) +│ +└──> Sort Node + │ + └──> Hash Join Node + │ + ├──> Seq Scan on users (Filtered) + │ + └──> Hash + │ + └──> Seq Scan on activities +``` + +PostgreSQL executes a plan top down, that is, it starts with producing the first result row for the top node. The executor processes lower nodes “on demand”, that is, it fetches only as many result rows from them as it needs to calculate the next result of the upper node. This influences how you have to read “cost” and “time”: the startup time for the upper node is at least as high as the startup time of the lower nodes, and the same holds for the total time. If you want to find the net time spent in a node, you have to subtract the time spent in the lower nodes. Parallel queries make that even more complicated. + +On top of that, you have to multiply the cost and the time with the number of “loops” to get the total time spent in a node. + +### Common nodes in PostgreSQL explain output + +| Node Type | Description | +| --------------------- | ---------------------------------------------------------------------------- | +| **Seq Scan** | Scans each row of a table sequentially, often used without suitable indexes. | +| **Index Scan** | Uses an index to quickly find rows, efficient for small fractions of rows. | +| **Index Only Scan** | Retrieves all needed data from the index itself, without visiting the table. | +| **Bitmap Heap Scan** | Uses a bitmap of row locations to efficiently retrieve rows from the table. | +| **Bitmap Index Scan** | Builds a bitmap by scanning the index to efficiently locate rows. | +| **Tid Scan** | Fetches rows directly using tuple identifiers, used in sub-selects. | +| **Nested Loop** | Joins two tables by scanning the first and then the second for each row. | +| **Merge Join** | Joins two pre-sorted tables, efficient for large datasets. | +| **Hash Join** | Uses a hash table to perform joins, often faster for larger datasets. | +| **Aggregate** | Performs aggregation calculations like `SUM`, `COUNT`, etc. | +| **Sort** | Sorts rows based on specified criteria, required for certain operations. | +| **Limit** | Returns a specified number of rows quickly, used with `LIMIT` clause. | +| **CTE Scan** | Scans a Common Table Expression, used for WITH clauses. | +| **Materialize** | Materializes the result of a subquery or node to reuse without re-running. | +| **Subquery Scan** | Executes and provides the results of a subquery to the outer query. | +| **Foreign Scan** | Fetches data from foreign data sources outside the local database. | +| **Function Scan** | Retrieves results from a set-returning function. | + +### What to focus on in explain analyze output + +- Find the nodes where most of the execution time was spent. + `Hash Join (cost=100.00..200.00 rows=1000 width=50) (actual time=50.012..150.023 rows=1000 loops=1)` + Explanation: + The actual time=50.012..150.023 indicates that this join operation took about 100 milliseconds to complete, making it a potential performance bottleneck. +- Find the lowest node where the estimated row count is significantly different from the actual row count. + `Seq Scan on users (cost=0.00..50.00 rows=100 width=50) (actual time=0.010..25.000 rows=10000 loops=1)` + Explanation: + The estimated rows are 100, but the actual rows are 10,000. This discrepancy may lead the query planner to make inefficient choices, such as choosing a sequential scan over an index scan. +- Find long-running sequential scans with a filter condition that removes many rows. + +``` +Seq Scan on products (cost=0.00..100.00 rows=300 width=50) (actual time=50.000..100.000 rows=3 loops=1) + Filter: (price > 1000) + Rows Removed by Filter: 2997 +``` + +Explanation: +This sequential scan took 50 to 100 milliseconds and filtered out 2997 of 3000 rows, indicating that only a few rows met the condition. This scenario is ideal for an index on the price column to optimize the performance by reducing the need for a full table scan. + +### Understanding the significance of milliseconds in Query Performance + +Determining whether 100 milliseconds for e.g is noteworthy in the context of identifying performance bottlenecks depends on various factors: + +**Overall Query Execution Time:** + +If the total execution time of a query significantly exceeds 100 milliseconds, then this particular step may not represent the main bottleneck. For instance, in queries that take several seconds to execute, a component that consumes just 100 milliseconds may not be the critical target for optimization efforts. + +**Complexity and Scale of the Query:** + +For complex queries that involve multiple joins, subqueries, or aggregation functions, an operation that takes 100 milliseconds might actually reflect good efficiency. Conversely, for simpler queries or operations expected to be quick (such as fetching a few rows from a well-indexed table), 100 milliseconds could suggest a lack of efficiency. + +So, the acceptable performance threshold can vary by application. For real-time systems or high-frequency trading platforms, even a few milliseconds can be critical, whereas for batch processing or data warehousing, longer execution times might be acceptable. + +### Tools to interpret explain analyze output + +Since reading a longer execution plan is quite cumbersome, you can use the website https://explain.depesz.com/ to better visualize the query. If you paste the execution plan in the text area and hit “Submit”, you will get output like this: +![image](/docs/img/troubleshooting/830b81aa-ae52-44a4-aa49-32c2a6296af7.png) + +### Tips for optimizing queries + +- **Add Indexes:** Improve performance by adding indexes on columns that are frequently used in WHERE clauses or JOIN conditions. + +- **Avoid Selecting Unused Columns:** Use SELECT to specify only the columns you need. + +- **Update Statistics:** Ensure that statistics are up to date to help the optimizer make better choices. + +### Conclusion + +Understanding EXPLAIN and EXPLAIN ANALYZE output can significantly enhance your ability to write efficient SQL queries. Regularly analyze query performance and make adjustments as your dataset grows and changes. + +For more insights, you can also check out: https://aws.amazon.com/blogs/database/how-postgresql-processes-queries-and-how-to-analyze-them/. diff --git a/apps/docs/content/troubleshooting/understanding-postgresql-logging-levels-and-how-they-impact-your-project-KXiJRm.mdx b/apps/docs/content/troubleshooting/understanding-postgresql-logging-levels-and-how-they-impact-your-project-KXiJRm.mdx new file mode 100644 index 00000000000..7dcb8b6c64c --- /dev/null +++ b/apps/docs/content/troubleshooting/understanding-postgresql-logging-levels-and-how-they-impact-your-project-KXiJRm.mdx @@ -0,0 +1,120 @@ +--- +title = "Understanding PostgreSQL Logging Levels and How They Impact Your Project" +github_url = "https://github.com/orgs/supabase/discussions/29877" +date_created = "2024-10-14T09:17:02+00:00" +topics = [ "database" ] +keywords = [ "logging", "disk", "i/o", "lockups" ] +database_id = "10186830-8cce-4f10-8cb9-7cbf39310763" +--- + +Since each Supabase project uses PostgreSQL as its underlying database engine, it’s common to adjust logging settings for various reasons—whether for debugging issues, monitoring database performance, or auditing actions. However, modifying logging levels improperly can lead to an excessive amount of log data being generated, which can quickly fill up your disk space and cause significant performance degradation or even system failure. + +### 1. Overview of PostgreSQL logging levels + +PostgreSQL provides multiple logging levels that allow you to control how much information gets logged. These include: + +| Log Level | Description | +| --------- | --------------------------------------------------------------------------------------------------------------------- | +| DEBUG1-5 | Logs very detailed information about the operations of the database, useful only for deep debugging. | +| INFO | Logs information about routine database operations that aren’t necessarily errors but may still be relevant to track. | +| NOTICE | Logs messages that are not errors but may still be noteworthy. | +| WARNING | Logs warnings, which indicate issues that don’t prevent execution but could cause problems later. | +| ERROR | Logs errors that cause statements to fail. | +| LOG | Logs general messages such as startup, shutdown, or checkpoints. | +| FATAL | Logs errors that cause the database session to fail. | +| PANIC | Logs critical issues that force the database to shut down. | + +Each of these log levels is useful in specific situations. Here's an example of what messages tagged with each severity level look like: + +``` +DEBUG: server process (PID 12345) exited with exit code 0 +INFO: vacuuming "example_schema.public.example_table" +NOTICE: identifier "very_very_very_long_table_name_exceeding_63_characters" will be truncated to "very_very_very_long_table_name_exceedin" +WARNING: SET LOCAL can only be used in transaction blocks +LOG: statement: UPDATE example_table SET column_name = 'Example Value' WHERE id = 10; +ERROR: relation "exam" does not exist at character 7 +FATAL: role "admin" does not exist +PANIC: database system shutdown requested +``` + +The default log level is set to **WARNING** through the log_min_messages setting, and we recommend keeping it that way. + +### 2. How high log levels can affect your database + +When users alter a high level of log settings, the database can start generating an overwhelming number of log entries. This can quickly escalate to issues such as: + +- Disk Space Exhaustion: Log files can grow exponentially if verbose levels like DEBUG, INFO, or NOTICE are enabled for long periods. And running out of disk space due to log bloat can cause your database to stop accepting writes and slow down query performance. + +- I/O Overload: Writing too many logs increases input/output (I/O) operations, which can negatively impact database speed. A database bogged down by heavy logging will take longer to process requests, leading to slower application performance. + +- Database Lockups: In extreme cases, if the disk is filled to capacity with logs, your database could lock up, leading to downtime or severe performance degradation. + +### 3. Common scenarios that cause log overload + +Here are a few common scenarios where excessive logging can become a problem: + +- Extended Use of DEBUG Logging: While useful for troubleshooting, leaving DEBUG logging enabled for a long period can lead to the accumulation of massive amounts of logs. + +- Setting INFO for Monitoring: Logging INFO can be helpful for tracking general database activity, but if left on indefinitely, it can still result in a lot of noise and unnecessary log growth. + +- Frequent Write Operations: If your database processes a lot of write operations (such as inserts, updates, or deletes), even low-level logs (like NOTICE or INFO) can lead to significant log accumulation. + +### 4. How to manage PostgreSQL log levels effectively + +**a. Choose the Right Log Level** +For most users, setting PostgreSQL logs to WARNING or ERROR is sufficient for regular operations. Here’s a general guideline: + +- WARNING: Use this level for general logging during normal operations. It captures issues that may not be critical but are worth paying attention to. + +- ERROR: This level is ideal for production environments. It logs only failures that prevent queries from being executed, reducing log noise significantly. + +- DEBUG, INFO, or NOTICE: Use these levels sparingly and only for short-term debugging or diagnostics. Always remember to revert the setting once you've collected the necessary information. + +**b. How to Adjust Log Levels** +You can adjust log levels using SQL commands in the SQL Editor or any connected PostgreSQL client: + +- To check the current log level: + +``` +SHOW log_min_messages; +``` + +- To set the log level to WARNING (recommended default): + +``` +ALTER ROLE postgres SET log_min_messages TO 'WARNING'; +``` + +- To set the log level to ERROR: + +``` +ALTER ROLE postgres SET log_min_messages TO 'ERROR'; +``` + +- To reset to the default level: + +``` +ALTER ROLE postgres RESET log_min_messages; +``` + +### 5. Conclusion + +PostgreSQL logs provide a powerful way to gain valuable insights into your database activity and performance when properly configured, but the key lies in finding the right balance. When set up properly, they can be incredibly useful. + +### 6. Other resources + +**a. What Events Are Logged in Postgres** +For a detailed explanation of the types of events logged in your database (such as connection events, checkpoint events, long-running queries, cron jobs, and severity-based logging), you can refer to the official documentation here: + +[What Events Are Logged in Supabase](https://gist.github.com/TheOtherBrian1/991d32c2b00dbc75d29b80d4cdf41aa7) + +**b. PGAudit: Postgres Auditing for Compliance and Security** + +We support PGAudit extension, which extends PostgreSQL’s built-in logging capabilities to track database activities for auditing purposes. + +[How to Enable the PGAudit Extension](https://supabase.com/docs/guides/database/extensions/pgaudit?queryGroups=database-method&database-method=dashboard#enable-the-extension) + +For detailed configuration instructions and logging options, refer to the complete documentation: [PGAudit Configuration Guide](https://supabase.com/docs/guides/database/extensions/pgaudit?queryGroups=database-method&database-method=dashboard#configure-the-extension) + +**c. Debugging Functions** +For more information on how to debug functions in Supabase, refer to the official guide: [Debugging Functions](https://supabase.com/docs/guides/database/functions?queryGroups=language&language=js#debugging-functions). diff --git a/apps/docs/content/troubleshooting/understanding-the-usage-summary-on-the-dashboard-D7Gnle.mdx b/apps/docs/content/troubleshooting/understanding-the-usage-summary-on-the-dashboard-D7Gnle.mdx new file mode 100644 index 00000000000..05639541bd5 --- /dev/null +++ b/apps/docs/content/troubleshooting/understanding-the-usage-summary-on-the-dashboard-D7Gnle.mdx @@ -0,0 +1,52 @@ +--- +title = "Understanding the Usage Summary on the Dashboard" +github_url = "https://github.com/orgs/supabase/discussions/27479" +date_created = "2024-06-24T00:26:53+00:00" +topics = [ "platform", "studio", "database", "functions" ] +keywords = [ "usage", "metrics", "projects" ] +database_id = "f379a628-6821-40b0-9793-4bf819b8c085" +--- + +## Organization usage metrics + +This discussion explains your organization's usage metrics, which can be found here: [https://supabase.com/dashboard/org/\_/usage](https://supabase.com/dashboard/org/_/usage). + +![](/docs/img/troubleshooting/46f59019-6ff1-4bfb-9acd-d5b7bd282f89.png) + +These metrics are calculated based on all the projects under the organization that meet these criteria: + +- Currently active projects +- Inactive projects that were active for some period of time within the current billing cycle + +This may be confusing for users looking at only what metrics are being used for currently active projects. + +## Database size example + +Using the database size as an example, suppose we have the following projects under the organization: + +- Project A: 1 GB database size +- Project B: 0.5 GB database size +- Project C: 0.5 GB database size + +If the user decides to delete Projects A and B during the current billing cycle, the average database usage will be 2 GB for the current billing cycle, even though only a 0.5 GB database is currently active. + +As of now, even if your project was just active for a few days, we take the average database size for those active days, which may lead to high usage when creating and deleting many projects within a billing cycle. We're working on a better billing model to cover these cases. + +## Edge Functions example + +Consider another example with edge functions. Suppose we have these projects under the organization: + +- Project D: 5 Edge Functions +- Project E: 7 Edge Functions + +If Project E with its 7 functions is deleted during the current billing cycle, the average Edge Function count for this billing cycle on the dashboard will still be 12 functions. + +## Other metrics + +It is a little more tricky to understand metrics like realtime and egress due to being more complex to track usage compared to Database size or Function count. However, they will account for usage from all projects that are active or were active during your current billing cycle as well. + +## Metrics still look weird or incorrect? + +If the numbers from inactive projects carry over to the next billing cycle and no new usage has occurred, something might be wrong. + +If this is the case, or if you are unsure why any usage appears the way it does for any reason, you should reach out to Support on the dashboard here: [https://supabase.com/dashboard/support/new](https://supabase.com/dashboard/support/new). diff --git a/apps/docs/content/troubleshooting/upload-file-size-restrictions-Y4wQLT.mdx b/apps/docs/content/troubleshooting/upload-file-size-restrictions-Y4wQLT.mdx new file mode 100644 index 00000000000..8b249e2b8c9 --- /dev/null +++ b/apps/docs/content/troubleshooting/upload-file-size-restrictions-Y4wQLT.mdx @@ -0,0 +1,42 @@ +--- +title = "Upload file size restrictions" +github_url = "https://github.com/orgs/supabase/discussions/27431" +date_created = "2024-06-20T23:15:48+00:00" +topics = [ "storage" ] +keywords = [ "upload", "file", "size", "restriction" ] +database_id = "3b0aa0f2-9608-4691-94d6-4fca8a32130e" +--- + +You can view the max permissible upload size for your plan in the [docs](https://supabase.com/docs/guides/storage/uploads/file-limits). + +## There are two ways to control the max upload sizes: + +The first way is through the [global storage settings](https://supabase.com/dashboard/project/_/settings/storage): + +Screenshot 2024-06-20 at 7 06 57 PM + +The second way is at the [bucket level](https://supabase.com/dashboard/project/_/storage/buckets/) + +1. Edit a bucket's configurations: + + Screenshot 2024-06-20 at 7 07 48 PM + +2. Change the file upload restriction if set: + Screenshot 2024-06-20 at 7 07 56 PM + +## Different upload methods impose file size restrictions: + +- [Standard uploads can only transfer up to 5GBs](https://supabase.com/docs/guides/storage/uploads/standard-uploads?queryGroups=language&language=js). However, for files above 6MB, the below methods are more performant and reliable +- [Resumable](https://supabase.com/docs/guides/storage/uploads/resumable-uploads) and [S3](https://supabase.com/docs/guides/storage/uploads/resumable-uploads) uploads can support transfers up to 50GB in size. diff --git a/apps/docs/content/troubleshooting/using-google-smtp-with-supabase-custom-smtp-ZZzU4Y.mdx b/apps/docs/content/troubleshooting/using-google-smtp-with-supabase-custom-smtp-ZZzU4Y.mdx new file mode 100644 index 00000000000..6462e8ebb12 --- /dev/null +++ b/apps/docs/content/troubleshooting/using-google-smtp-with-supabase-custom-smtp-ZZzU4Y.mdx @@ -0,0 +1,36 @@ +--- +title = "Using Google SMTP with Supabase Custom SMTP" +github_url = "https://github.com/orgs/supabase/discussions/19646" +date_created = "2023-12-13T01:40:37+00:00" +topics = [ "auth", "platform" ] +keywords = [ "smtp", "google", "workspace", "2FA", "TLS", "encryption", "port" ] +database_id = "4d5390a0-445d-4e0a-ae7e-9b713b7cc4f6" +--- + +Hey everyone, i've been hearing feedback about how challenging it can be to get google SMTP working properly with Supabase. I've tried setting this up with a trial google workspace account and this is what i've discovered. + +1. The sender email and SMTP username has to be your google workspace admin email. +2. The SMTP password used has to be an app password. Google workspace doesn't make it easy to to figure out how to create this. You need to **enable 2FA** on your workspace account first (not inside the admin console but in https://myaccount.google.com/) +3. If you want to use `smtp-relay.gmail.com` only port 465 works. If you want to use `smtp.gmail.com`, you can use port 465 or 587 (both ports worked for me). + +I'm also gonna include some screenshots below on what those steps look like: + +## For `smtp-relay.gmail.com` + +![image](/docs/img/troubleshooting/800448a1-0245-4a26-855f-3be8f44adf03.png) + +(The "Require TLS encryption" option doesn't seem to affect anything during my testing) +![image](/docs/img/troubleshooting/5b55ddbc-abfc-4c66-a012-b8e4d23cd438.png) + +## For `smtp.gmail.com` + +![image](/docs/img/troubleshooting/ade24c85-50b5-47ca-a2ce-c3f77a0e128a.png) + +A lot of this was figured out through trial and error - i don't have explanations as to why only port 465 works with `smtp-relay.gmail.com` and / or why the TLS encryption option has no effect on SMTP. + +As to whether this is a Supabase Auth bug, i highly doubt that it is because we use the [native golang smtp library ](https://pkg.go.dev/net/smtp). If anyone still has issues getting google SMTP to work, i would recommend to switch to one of these SMTP providers: + +- [Resend](https://resend.com/blog/how-to-configure-supabase-to-send-emails-from-your-domain) +- [Sendgrid](https://sendgrid.com/en-us) +- [Mailgun](https://www.mailgun.com/) +- AWS SES SMTP diff --git a/apps/docs/content/troubleshooting/using-sqlalchemy-with-supabase-FUqebT.mdx b/apps/docs/content/troubleshooting/using-sqlalchemy-with-supabase-FUqebT.mdx new file mode 100644 index 00000000000..a868ed5e79a --- /dev/null +++ b/apps/docs/content/troubleshooting/using-sqlalchemy-with-supabase-FUqebT.mdx @@ -0,0 +1,90 @@ +--- +title = "Using SQLAlchemy with Supabase" +github_url = "https://github.com/orgs/supabase/discussions/27071" +date_created = "2024-06-06T16:27:43+00:00" +topics = [ "database", "supavisor", "self-hosting", "functions" ] +keywords = [ "sqlalchemy", "ipv6", "pool", "database", "connection" ] +database_id = "da4efcda-cd02-4001-965b-f0294300152f" +--- + +## Deploying to auto-scaling servers: + +If you are deploying to: + +- edge functions +- serverless functions +- horizontally auto-scaling deployments + +It is recommended that you connect with the pooler in transaction mode (port 6543), which can be found in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database): + +```sh +# Example transaction mode string: +postgres://[db-user].[project-ref]:[db-password]@aws-0-[aws-region].pooler.supabase.com:6543 +``` + +When using transaction mode, you should use the[ NullPool setting:](https://docs.sqlalchemy.org/en/20/core/pooling.html#switching-pool-implementations) + +```py +from sqlalchemy.pool import NullPool + +con = sqlalchemy.create_engine(url, client_encoding='utf8', poolclass=NullPool) +``` + +When relying on Supavisor, it's important to pick an adequate pool size. This guide can walk you through the process: + +- https://github.com/orgs/supabase/discussions/21566 + +## Deploying to stationary servers + +For stationary servers, such as VMs and long-running containers, it is recommended to use your direct connection string, which can be found in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) + +``` +# Example DB string: +postgresql://postgres:[PASSWORD]@db.[PROJECT REF].supabase.co:5432/postgres +``` + +The connection maps to an IPv6 address, and cannot operate in an IPv4 environment. + +### Checking IPv6 support: + +The majority of services are IPv6 compatible. However, there are a few prominent services that only accept IPv4 connections: + +- [Retool](https://retool.com/) +- [Vercel](https://vercel.com/) +- [GitHub Actions](https://docs.github.com/en/actions) +- [Render](https://render.com/) + +If you're still unsure if your network supports IPv6, you can run this cURL command on your deployment server: + +```sh +curl -6 https://ifconfig.co/ip +``` + +If the command returns an IPv6 address, the network is IPv6 compatible. + +If your deployment environment is not IPv6 compatible, then consider: + +- Using the Supavisor pooler in session (port 5432) +- Enabling the [IPv4 Add-On](https://supabase.com/dashboard/project/_/settings/addons) if you're on a pro or above plan + +### Choosing an internal pool size + +**Key Pool Settings:** + +- pool_size: This sets the maximum number of permanent connections in the pool. SQLAlchemy will create connections as needed up to this limit. + max_overflow: Allows creating additional connections beyond pool_size for temporary bursts in demand. These temporary connections close after use. + +```python +# Example configurations +engine = create_engine( +    "postgresql+psycopg2://me@localhost/mydb", pool_size=20, max_overflow=15 +) +``` + +As a rule of thumb, if you're using the Supabase Database REST Client, try to limit the connections used by your deployment to 40% of available connections. Otherwise, you can cautiously increase usage to around 80%. These percentages are flexible and depend on your application's usage and setup. Monitor connection usage to determine the optimal allocation without depriving other servers of necessary connections. + +## How to monitor live connections + +Connection usage can be monitored with a Supabase Grafana Dashboard. It provides realtime visibility of over 200 database metrics, such as graphs of CPU, EBS, and active direct/pooler connections. It can be extremely useful for monitoring and debugging instances. + +You can check our [GitHub repo](https://github.com/supabase/supabase-grafana) for setup instructions for local deployments or free cloud deployments on [Fly.io](http://fly.io/). For a complete explainer on connection monitoring, you can check out this [guide](https://github.com/orgs/supabase/discussions/27141) diff --git a/apps/docs/content/troubleshooting/webhook-debugging-guide-M8sk47.mdx b/apps/docs/content/troubleshooting/webhook-debugging-guide-M8sk47.mdx new file mode 100644 index 00000000000..80b573af650 --- /dev/null +++ b/apps/docs/content/troubleshooting/webhook-debugging-guide-M8sk47.mdx @@ -0,0 +1,88 @@ +--- +title = "Webhook debugging guide" +github_url = "https://github.com/orgs/supabase/discussions/21023" +date_created = "2024-02-05T17:56:07+00:00" +topics = [ "database" ] +keywords = [ "webhook", "timeout", "pg_net", "postgres", "trigger" ] +database_id = "a5524182-9dc4-4470-86fe-45ace109a53a" +--- + +> NOTE: version 0.10.0 of pg*net is out. You should consider updating your database in the[ Infrastructure Settings](https://supabase.com/dashboard/project/*/settings/infrastructure) if you are on a prior version. If you do not know your version, you can check the [Extensions Dashboard](https://supabase.com/dashboard/project/_/database/extensions). + +**Debugging Steps** + +**1. Test if webhooks are active:** +Webhooks are run in a Postgresql background worker. The first debugging step is to see if the worker is active. Run the following SQL code: + +```sql +select pid from pg_stat_activity where backend_type ilike '%pg_net%'; +``` + +If it does not return an integer, then the worker has failed and needs to be restarted. If you are running PG_NET 0.8 or later, you can restart the background worker by executing the following function: + +```sql +select net.worker_restart(); +``` + +Otherwise, it is necessary to fast reboot your instance in the [Dashboard's Settings](https://supabase.com/dashboard/project/_/settings/general). + +**2. Remove any triggers on net tables:** + +Using `pg_net` in triggers on most tables is fine; however, please do not add triggers to the `net._http_response` or `net.http_request_queue` tables. + +The `net` tables are special and if triggers on them fail or call a pg_net function (`http_get`, `http_post`, `http_delete`), it can lead to an infinite loop. This warning is irrelevant to most projects, but it's worth specifying just in case. + +**3. Check for timeout errors** + +> **NOTE**: The timeout issue has been patched in pg*net v0.11. It is available in Postgres 15.6.1.135 and above. You can upgrade your version of Postgres in the [Infrastructure Settings.](https://supabase.com/dashboard/project/*/settings/infrastructure) + +Go to your [Table Editor](https://supabase.com/dashboard/project/_/editor/) and navigate to the net schema. It will contain two tables: + +- `_http_response` +- `http_request_queue` + +The `_http_response` table saves all the response messages from the past 6 hours. If every column contains NULL values, except for the `id`, `error_msg`, and `created` columns, then you are experiencing [a timeout bug](https://github.com/supabase/pg_net/issues/86). + +By default, webhooks will execute the next 200 available queued requests. If the requests are too intense, it may result in a mass timeout. In the [Webhook Dashboard](https://supabase.com/dashboard/project/_/database/hooks), you should increase your webhook's timeout to minimize this issue. + +**4. Inspect endpoints** +The below code makes a request to the Postman Echo API through PG_NET + +```sql +select +net.http_post( +url := 'https://postman-echo.com/post', +body := '{"key1": "value", "key2": 5}'::jsonb +) as request_id; +``` + +Postman will then respond with the same payload. This is just a test to confirm that requests are being properly formatted and going through. + +You can then view the request in the `net._http_response table` in the [Table Editor](https://supabase.com/dashboard/project/_/editor) or with the following SQL: + +```sql +select +* +from net._http_response +where id = +``` + +You should also inspect all the failed responses from the past 6 hours to see if their are any insightful messages: + +```sql +select + * +from net._http_response +where "status_code" >= 400 or "error_msg" is not null +order by created desc; +``` + +Status codes from a server are described in [Mozilla's web docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) + +**5. Create logs** + +If none of the above suggestions uncovered the cause of the error, for debugging purposes, it may be useful to write a custom webhook that can create logs in the [Dashboard's Postgres Logs](https://supabase.com/dashboard/project/_/logs/postgres-logs). The process is outlined in the [PG_NET](https://supabase.com/docs/guides/database/extensions/pg_net#debugging-requests) documentation. + +**Conclusion:** + +If your issue still persists, please document it as an issue in the [PG_NET Github Repo](https://github.com/supabase/pg_net/issues). You are also welcome to contact Supabase Support from your project's [Dashboard](https://supabase.com/dashboard/project/_/) for more guidance. diff --git a/apps/docs/content/troubleshooting/why-am-i-being-redirected-to-the-wrong-url-when-using-auth-redirectto-option-_vqIeO.mdx b/apps/docs/content/troubleshooting/why-am-i-being-redirected-to-the-wrong-url-when-using-auth-redirectto-option-_vqIeO.mdx new file mode 100644 index 00000000000..423b1fdefaf --- /dev/null +++ b/apps/docs/content/troubleshooting/why-am-i-being-redirected-to-the-wrong-url-when-using-auth-redirectto-option-_vqIeO.mdx @@ -0,0 +1,15 @@ +--- +title = "Why am I being redirected to the wrong url when using auth redirectTo option?" +github_url = "https://github.com/orgs/supabase/discussions/12946" +date_created = "2023-03-10T17:15:10+00:00" +topics = [ "auth" ] +database_id = "d4b45e90-9c52-4781-8cc2-d936e8f4edeb" +--- + +Also, Why am I getting redirected to localhost instead of my production site URL? + +In order for the provided redirectTo option to work you must set the exact URL in the shown Redirect URL's setting. + +![image](/docs/img/troubleshooting/224379580-fa77bd31-bb58-47e6-90ce-64e140f32579.png) + +For more information on formats for redirect URL settings see the documentation here: https://supabase.com/docs/guides/auth/overview#redirect-urls-and-wildcards diff --git a/apps/docs/content/troubleshooting/why-are-there-gaps-in-my-postgres-id-sequence-Frifus.mdx b/apps/docs/content/troubleshooting/why-are-there-gaps-in-my-postgres-id-sequence-Frifus.mdx new file mode 100644 index 00000000000..83a866c42fc --- /dev/null +++ b/apps/docs/content/troubleshooting/why-are-there-gaps-in-my-postgres-id-sequence-Frifus.mdx @@ -0,0 +1,79 @@ +--- +title = "Why are there gaps in my Postgres id sequence?" +github_url = "https://github.com/orgs/supabase/discussions/22299" +date_created = "2024-03-27T18:04:39+00:00" +topics = [ "database" ] +keywords = [ "sequence", "id", "rollback", "deletions", "upserts" ] +database_id = "0c0369db-2968-4ea7-9f99-c5f5ce55acad" + +[[errors]] +message = "Failed to run sql query: duplicate key value violates unique constraint \"{table}_pkey\"" +--- + +Most database tables have a primary key that is a made-up number, and that number is usually made by a sequence. + +You might be surprised to know that gaps in sequence IDs are a normal aspect of working with relational databases in general. It's a common scenario that can seem puzzling at first, but there are several reasons why these gaps occur, and sometimes, this is typically expected and does not indicate a problem with your database. + +It's also important to understand the distinction that sequences guarantee **uniqueness**, but not **consecutiveness**, and this should not imply any issues relating to data integrity with your database. + +### How to check the name of your sequence + +If you don't know the name of your sequence, it's often formed based on a standard naming convention: table_name_id_seq, where table_name is the name of your table and id is the name of your serial column. + +### Common reasons for gaps in sequences + +1. Rollbacks + One of the most common reasons for a gap is the rollback of a transaction. If you initiate a transaction that includes an insert operation, the sequence responsible for generating the ID for the new row increments. If, for any reason, the transaction doesn't complete successfully—perhaps due to a constraint violation or a deliberate decision to rollback—the insert operation is undone, but the sequence value used is not returned or reused. The documentation explains that as well: + +> To avoid blocking concurrent transactions that obtain numbers from the same sequence, a nextval operation is never rolled back; that is, once a value has been fetched it is considered used and will not be returned again. This is true even if the surrounding transaction later aborts, or if the calling query ends up not using the value. For example an INSERT with an ON CONFLICT clause will compute the to-be-inserted tuple, including doing any required nextval calls, before detecting any conflict that would cause it to follow the ON CONFLICT rule instead. Such cases will leave unused “holes” in the sequence of assigned values. + +2. Deletions + Removing rows from your table also creates gaps in the sequence of primary key values. Although deletions do not affect the sequence directly, they contribute to the non-sequential appearance of IDs in your table. + +3. Manual Sequence Adjustments + Another source of gaps can be manual interventions, such as incrementing a sequence number directly or setting it to a new value. + +4. Upserts + When an upsert is executed, it can still increase the sequence even if it is set to do nothing on conflicts + +### Checking for gaps + +To check for gaps in the sequence of IDs in a PostgreSQL table, you can use a SQL query that compares the sequence of IDs to a generated series of numbers that spans the same range: + +``` +SELECT + s.id AS missing_id +FROM + generate_series((SELECT MIN(id) FROM your_table), (SELECT MAX(id) FROM your_table)) s(id) + LEFT JOIN your_table ON your_table.id = s.id +WHERE + your_table.id IS NULL; +``` + +This query should help you pinpoint where gaps exist. + +### Encountering errors and adjusting sequences + +In operations involving sequences, you might encounter an error message such as: + +`Failed to run sql query: duplicate key value violates unique constraint "{table}_pkey"` + +This error suggests a discrepancy between the sequence-generated IDs and the actual IDs in the table. + +To address this, you can check the current value of your sequence or the highest ID in your table with: + +``` +postgres=# SELECT max(id) FROM ; + +postgres=# SELECT nextval('{table}_{column}_seq'); +``` + +And reset the sequence value to match the highest ID plus one: +`SELECT setval('{table}_{column}_seq', (SELECT max(id) FROM ) + 1);` + +Or, alternatively, adjust the sequence to a specific new value: +`ALTER SEQUENCE '{table}_{column}_seq' RESTART WITH new_value;` + +### Implementing a gapless ID sequence + +If your application requires contiguous IDs and you decide to build a gapless sequence. Think twice about this, as it will serialize all transactions that use that “sequence” which will then deteriorate your data modification performance considerably. However, if your application truly requires gapless IDs, then you can use a Trigger: After a successful insert, use a database trigger to assign an ID based on a custom logic that finds the next available gapless ID. diff --git a/apps/docs/content/troubleshooting/why-cant-i-uploadlistetc-my-public-bucket-Z6CmGt.mdx b/apps/docs/content/troubleshooting/why-cant-i-uploadlistetc-my-public-bucket-Z6CmGt.mdx new file mode 100644 index 00000000000..cde925c470c --- /dev/null +++ b/apps/docs/content/troubleshooting/why-cant-i-uploadlistetc-my-public-bucket-Z6CmGt.mdx @@ -0,0 +1,11 @@ +--- +title = "Why can't I upload/list/etc my public bucket?" +github_url = "https://github.com/orgs/supabase/discussions/19180" +date_created = "2023-11-23T16:52:55+00:00" +topics = [ "storage" ] +database_id = "8abd4a38-041b-4620-95af-53673f698dea" +--- + +A public bucket in the storage API only means there is a public URL available you can use to download the file. +All other bucket or file operations require you to meet storage policies on that bucket. +Note also you should use the normal path for all other operations and not the public URL path with 'public' in it. diff --git a/apps/docs/content/troubleshooting/why-do-i-see-auth--api-requests-in-the-dashboard-my-app-has-no-users-CyadiO.mdx b/apps/docs/content/troubleshooting/why-do-i-see-auth--api-requests-in-the-dashboard-my-app-has-no-users-CyadiO.mdx new file mode 100644 index 00000000000..e048e0b8265 --- /dev/null +++ b/apps/docs/content/troubleshooting/why-do-i-see-auth--api-requests-in-the-dashboard-my-app-has-no-users-CyadiO.mdx @@ -0,0 +1,20 @@ +--- +title = "Why do I see Auth & API requests in the dashboard? My app has no users" +github_url = "https://github.com/orgs/supabase/discussions/21579" +date_created = "2024-02-26T21:39:41+00:00" +topics = [ "auth", "platform", "database", "realtime", "functions" ] +keywords = [ "dashboard", "health", "endpoints" ] +database_id = "f85935d8-e871-4463-8288-f118f4e24afd" +--- + +The dashboard makes requests to the health endpoints of the supabase services (Database, Auth, Data API, Realtime, Edge Functions to ensure everything is working). These requests appear in the charts about your project: + +image + +You can see these requests to the health endpoints from the log explorer here: + +https://supabase.com/dashboard/project/_/logs/edge-logs diff --git a/apps/docs/content/troubleshooting/why-is-my-camelcase-name-not-working-in-postgres-functions-or-rls-policies-EJMzVd.mdx b/apps/docs/content/troubleshooting/why-is-my-camelcase-name-not-working-in-postgres-functions-or-rls-policies-EJMzVd.mdx index 889a5c2fc09..f249fa2304d 100644 --- a/apps/docs/content/troubleshooting/why-is-my-camelcase-name-not-working-in-postgres-functions-or-rls-policies-EJMzVd.mdx +++ b/apps/docs/content/troubleshooting/why-is-my-camelcase-name-not-working-in-postgres-functions-or-rls-policies-EJMzVd.mdx @@ -3,6 +3,7 @@ title = "Why is my camelCase name not working in Postgres functions or RLS polic github_url = "https://github.com/orgs/supabase/discussions/12893" date_created = "2023-03-08T16:26:51+00:00" topics = [ "database" ] +keywords = ["rls", "rpc"] database_id = "9dbcf760-7b1f-4cc4-8653-4e884fd02dad" --- diff --git a/apps/docs/content/troubleshooting/why-is-my-select-returning-an-empty-data-array-and-i-have-data-in-the-table-xvOPgx.mdx b/apps/docs/content/troubleshooting/why-is-my-select-returning-an-empty-data-array-and-i-have-data-in-the-table-xvOPgx.mdx new file mode 100644 index 00000000000..ec43a9b2e05 --- /dev/null +++ b/apps/docs/content/troubleshooting/why-is-my-select-returning-an-empty-data-array-and-i-have-data-in-the-table-xvOPgx.mdx @@ -0,0 +1,41 @@ +--- +title = "Why is my select returning an empty data array and I have data in the table?" +github_url = "https://github.com/orgs/supabase/discussions/12895" +date_created = "2023-03-08T16:38:02+00:00" +topics = [ "database", "auth" ] +keywords = [ "rls", "policy", "filter", "jwt" ] +database_id = "6fa68831-6db8-4836-a1b4-e9e51296d529" +--- + +Usually this means you have RLS (row level security) enabled and no policy, or do not meet the policy. It can also mean you have a filter and have no rows matching that. + +If you have RLS enabled you can test quickly by disable RLS on the table. If your query works then you have no policies or do not meet them. + +If you have policies that depend on having a signed in user (TO set to `authenticated` or using `auth.uid()`) then you can check by setting TO as `anon` and setting your policy to just `TRUE`. If that works then you don't have a signed in user (with a jwt in the authorization header) when you make the call. + +For more information on RLS please see https://supabase.com/docs/guides/auth/row-level-security + +Adding: +To test if you have a user session at time of your call you can add this function with the SQL editor and an rpc call in your code at same place as your current database call. + +```sql +create function test_authorization_header() returns json + language SQL + as +$$ + select auth.jwt(); +$$; +``` + +```javascript +const { data: testData, error: testError } = await supabase.rpc('test_authorization_header') +console.log(`The user role is ${testData.role} and the user UUID is ${testData.sub}. `, testError) +``` + +If you have anon or service_role as the role then you do not have a user session at the time of your call. + +To delete the test SQL function: + +```sql +drop function test_authorization_header; +``` diff --git a/apps/docs/content/troubleshooting/why-is-my-service-role-key-client-getting-rls-errors-or-not-returning-data-7_1K9z.mdx b/apps/docs/content/troubleshooting/why-is-my-service-role-key-client-getting-rls-errors-or-not-returning-data-7_1K9z.mdx new file mode 100644 index 00000000000..105ffead6c5 --- /dev/null +++ b/apps/docs/content/troubleshooting/why-is-my-service-role-key-client-getting-rls-errors-or-not-returning-data-7_1K9z.mdx @@ -0,0 +1,20 @@ +--- +title = "Why is my service role key client getting RLS errors or not returning data?" +github_url = "https://github.com/orgs/supabase/discussions/30146" +date_created = "2024-10-28T18:31:39+00:00" +topics = [ "auth", "database" ] +keywords = [ "rls", "service_role", "authorization", "session", "apikey" ] +database_id = "677f0a69-e454-4718-ad92-91053d40c085" +--- + +A Supabase client with the Authorization header set to the service role apikey will ALWAYS bypass RLS. By default the Authorization header is the apikey used in createClient. If you are getting an RLS error then you have a user session getting into the client or you initialized with the anon key. RLS in enforced based on the Authorization header and not the apikey header. + +Three common cases of the createClient apikey being replaced by a user session/token: + +1. SSR client initialized with service role. The SSR clients are designed to share the user session from cookies. The user session will override the default apikey from createClient in the Authorization header. If you are using SSR, always create a separate server client using supabase-js directly for service role. + +2. Edge functions or other server code setting the Authorization header in createClient options directly to a user token/jwt. When you set the Authorization header directly that overrides the default action of using the apikey for the Authorization header. + +3. Server client initialized with service role using signUp to create a user or other auth functions. Many auth functions will return a user session to the client making the call. When that happens the apikey will be replaced by the user token/jwt in the Authorization header. If you are wanting to create a user in a service role client use admin.createUser() instead. Otherwise use a separate Supabase client for for service role from other actions. + +Also note that adding service_role in RLS policies does nothing. Service role will never run the policies to begin with. diff --git a/apps/docs/content/troubleshooting/why-is-my-supabase-api-call-not-returning-PGzXw0.mdx b/apps/docs/content/troubleshooting/why-is-my-supabase-api-call-not-returning-PGzXw0.mdx new file mode 100644 index 00000000000..b476e187b78 --- /dev/null +++ b/apps/docs/content/troubleshooting/why-is-my-supabase-api-call-not-returning-PGzXw0.mdx @@ -0,0 +1,14 @@ +--- +title = "Why is my supabase API call not returning?" +github_url = "https://github.com/orgs/supabase/discussions/19058" +date_created = "2023-11-17T22:39:54+00:00" +topics = [ "auth", "platform" ] +keywords = [ "deadlock", "hang", "async" ] +database_id = "0a6f6d33-7331-4b47-87ec-7ade7a6a5924" +--- + +There is currently a bug in supabase-js which results in a deadlock if any async API call is made in onAuthStateChange code. If a call is made in the handler then the next Supabase call anywhere using that client will hang and not return. + +For now a workaround is needed until this is fixed. + +https://github.com/supabase/gotrue-js/issues/762#issuecomment-1780006492 diff --git a/apps/docs/content/troubleshooting/will-backups-be-accessible-from-the-dashboard-immediately-after-upgrading-to-a-paid-plan-hXY4rs.mdx b/apps/docs/content/troubleshooting/will-backups-be-accessible-from-the-dashboard-immediately-after-upgrading-to-a-paid-plan-hXY4rs.mdx new file mode 100644 index 00000000000..1aa0a598d25 --- /dev/null +++ b/apps/docs/content/troubleshooting/will-backups-be-accessible-from-the-dashboard-immediately-after-upgrading-to-a-paid-plan-hXY4rs.mdx @@ -0,0 +1,12 @@ +--- +title = "Will backups be accessible from the dashboard immediately after upgrading to a paid plan?" +github_url = "https://github.com/orgs/supabase/discussions/22713" +date_created = "2024-04-14T16:05:26+00:00" +topics = [ "platform" ] +keywords = [ "backups", "upgrade", "dashboard" ] +database_id = "fa5bb840-a5ac-447f-b9bb-e319cbe0797e" +--- + +We are currently taking up to 7 daily backups that will be available for you once you upgrade. Please keep in mind that we might no longer make daily backups for free projects in the future. + +But at the moment, you'll be able to access them after upgrading. diff --git a/apps/docs/features/app.providers.tsx b/apps/docs/features/app.providers.tsx index 55b37d0ee0b..d07b3fda3cd 100644 --- a/apps/docs/features/app.providers.tsx +++ b/apps/docs/features/app.providers.tsx @@ -1,17 +1,16 @@ import { type PropsWithChildren } from 'react' -import { ThemeProvider } from 'common' +import { FeatureFlagProvider, IS_PLATFORM, ThemeProvider } from 'common' import { SonnerToaster } from 'ui' import { CommandProvider } from 'ui-patterns/CommandMenu' -// import { PromoToast } from 'ui-patterns/PromoToast' import SiteLayout from '~/layouts/SiteLayout' +import { API_URL } from '~/lib/constants' import { AuthContainer } from './auth/auth.client' import { DocsCommandMenu } from './command' import { QueryClientProvider } from './data/queryClient.client' import { PageTelemetry } from './telemetry/telemetry.client' import { ScrollRestoration } from './ui/helpers.scroll.client' import { ThemeSandbox } from './ui/theme.client' -import { PromoToast } from 'ui-patterns' /** * Global providers that wrap the entire app @@ -20,21 +19,22 @@ function GlobalProviders({ children }: PropsWithChildren) { return ( - - - - -
- - - {children} - - - -
-
- -
+ + + + + +
+ + {children} + + + +
+
+ +
+
) diff --git a/apps/docs/features/directives/utils.ts b/apps/docs/features/directives/utils.ts index d52e1fd94cd..e4b172272e0 100644 --- a/apps/docs/features/directives/utils.ts +++ b/apps/docs/features/directives/utils.ts @@ -8,7 +8,6 @@ import { mdxjs } from 'micromark-extension-mdxjs' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' -import { getGitHubFileContents } from '~/lib/octokit' import { getGitHubFileContentsImmutableOnly } from '~/lib/octokit' import { codeSampleRemark } from './CodeSample' diff --git a/apps/docs/features/docs/Troubleshooting.script.mjs b/apps/docs/features/docs/Troubleshooting.script.mjs index 698525aaea3..cd13d7d48c9 100644 --- a/apps/docs/features/docs/Troubleshooting.script.mjs +++ b/apps/docs/features/docs/Troubleshooting.script.mjs @@ -125,7 +125,7 @@ async function syncTroubleshootingEntries() { if (result.status === 'rejected') { console.error( `[ERROR] Failed to insert and/or update for ${troubleshootingEntries[index].filePath}:\n%O`, - result.reason + result.reason?.errors ?? result.reason ) hasErrors = true } @@ -169,7 +169,14 @@ function calculateChecksum(content) { extensions: [gfm(), mdxjs()], mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()], }) - const normalized = toMarkdown(mdast, { extensions: [gfmToMarkdown(), mdxToMarkdown()] }) + const bodyNormalized = toMarkdown(mdast, { extensions: [gfmToMarkdown(), mdxToMarkdown()] }) + + const { data, content: body } = matter(bodyNormalized, { + language: 'toml', + engines: { toml: toml.parse.bind(toml) }, + }) + const newFrontmatter = stringify(data) + const normalized = `---\n${newFrontmatter}\n---\n${body}` return createHash('sha256').update(normalized).digest('base64') } @@ -261,12 +268,19 @@ function addCanonicalUrl(entry) { return content } +/** + * @param {string} str + */ +function escapeGraphQlString(str) { + return str.replace(/"/g, '\\"') +} + /** * @param {TroubleshootingEntry} entry */ async function createGithubDiscussion(entry) { console.log(`[INFO] Creating GitHub discussion for ${entry.data.title}`) - const content = addCanonicalUrl(entry) + const content = escapeGraphQlString(addCanonicalUrl(entry)) const mutation = ` mutation { @@ -364,7 +378,7 @@ async function updateGithubDiscussion(entry) { throw error } - const content = addCanonicalUrl(entry) + const content = escapeGraphQlString(addCanonicalUrl(entry)) const mutation = ` mutation { updateDiscussion(input: { diff --git a/apps/docs/features/docs/Troubleshooting.ui.client.tsx b/apps/docs/features/docs/Troubleshooting.ui.client.tsx index 276262a614c..9ba084e6b74 100644 --- a/apps/docs/features/docs/Troubleshooting.ui.client.tsx +++ b/apps/docs/features/docs/Troubleshooting.ui.client.tsx @@ -2,7 +2,7 @@ import { ChevronDown, RotateCw, Search, X } from 'lucide-react' import { useQueryStates } from 'nuqs' -import { useEffect, useRef, useState, Suspense, useCallback } from 'react' +import { useEffect, useRef, useState, Suspense, useCallback, useMemo } from 'react' import { Input_Shadcn_, @@ -14,7 +14,6 @@ import { } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' -import { MultiSelect } from '~/components/MultiSelect.client' import { MultiSelector, MultiSelectorContent, @@ -107,7 +106,9 @@ function entryMatchesFilter( selectedTags.length === 0 || selectedTags.some((tag) => dataKeywords?.includes(tag)) const errorsMatch = selectedErrorCodes.length === 0 || - selectedErrorCodes.some((error) => dataErrors.includes(error)) + selectedErrorCodes.some((error) => + dataErrors.some((errorCode) => errorCode.includes(error.toString())) + ) const searchMatch = searchState === '' || content.toLowerCase().includes(searchState.toLowerCase()) @@ -200,6 +201,25 @@ function TroubleshootingFilterInternal({ allEntries.current = entries }, []) + const allErrorCodes: string[] = useMemo( + () => + Array.from( + new Set( + errors?.flatMap((error) => { + const result: string[] = [] + if (error.http_status_code) { + result.push(String(error.http_status_code)) + } + if (error.code) { + result.push(error.code) + } + return result + }) ?? [] + ) + ), + [errors] + ) + return ( <>

Search and filter

@@ -220,9 +240,9 @@ function TroubleshootingFilterInternal({ - {errors?.map((error) => ( - - {error.code} + {allErrorCodes.map((error) => ( + + {error} ))} diff --git a/apps/docs/features/docs/Troubleshooting.utils.ts b/apps/docs/features/docs/Troubleshooting.utils.ts index 6dc915d8628..4189c9c5da7 100644 --- a/apps/docs/features/docs/Troubleshooting.utils.ts +++ b/apps/docs/features/docs/Troubleshooting.utils.ts @@ -27,7 +27,9 @@ export interface ITroubleshootingEntry { export const getArticleSlug = getArticleSlugInternal async function getAllTroubleshootingEntriesTyped() { - const result = (await getAllTroubleshootingEntriesInternal()) as ITroubleshootingEntry[] + const result: ITroubleshootingEntry[] = ( + IS_PLATFORM ? await getAllTroubleshootingEntriesInternal() : [] + ) as ITroubleshootingEntry[] return result } export const getAllTroubleshootingEntries = cache_fullProcess_withDevCacheBust( diff --git a/apps/docs/features/helpers.fetch.ts b/apps/docs/features/helpers.fetch.ts index 55f2de27a93..494e0444fe4 100644 --- a/apps/docs/features/helpers.fetch.ts +++ b/apps/docs/features/helpers.fetch.ts @@ -18,9 +18,11 @@ function fetchWithNextOptions({ return (info: RequestInfo) => fetch(info, { next, cache }) } -// const fetchRevalidatePerDay = fetchWithNextOptions({ next: { revalidate: ONE_DAY_IN_SECONDS } }) +const fetchRevalidatePerDay_TEMP_TESTING = fetchWithNextOptions({ + next: { revalidate: ONE_DAY_IN_SECONDS }, +}) // [Charis 2024-12-28] // Temporarily disabling revalidation as a hotfix for Vercel NFT problem const fetchRevalidatePerDay = fetch -export { fetchWithNextOptions, fetchRevalidatePerDay } +export { fetchWithNextOptions, fetchRevalidatePerDay, fetchRevalidatePerDay_TEMP_TESTING } diff --git a/apps/docs/features/telemetry/telemetry.client.tsx b/apps/docs/features/telemetry/telemetry.client.tsx index 5264e668827..ee1853b8d74 100644 --- a/apps/docs/features/telemetry/telemetry.client.tsx +++ b/apps/docs/features/telemetry/telemetry.client.tsx @@ -1,86 +1,19 @@ 'use client' -import { isBrowser, useTelemetryCookie } from 'common' -import { usePathname } from 'next/navigation' -import { useCallback, useEffect } from 'react' +import { IS_PLATFORM, PageTelemetry as PageTelemetryImpl } from 'common' import { useConsent } from 'ui-patterns/ConsentToast' -import { IS_PLATFORM } from '~/lib/constants' -import { unauthedAllowedPost } from '~/lib/fetch/fetchWrappers' - -const useTelemetryWithConsent = () => { - const pathname = usePathname() - const { hasAcceptedConsent } = useConsent() - - const sendPageTelemetry = useCallback(() => { - if (!(IS_PLATFORM && hasAcceptedConsent)) return - - const title = typeof document !== 'undefined' ? document?.title : '' - const referrer = typeof document !== 'undefined' ? document?.referrer : '' - - unauthedAllowedPost('/platform/telemetry/page', { - body: { - pathname, - page_url: isBrowser ? window.location.href : '', - page_title: title, - ph: { - referrer, - language: navigator.language ?? 'en-US', - user_agent: navigator.userAgent, - search: isBrowser ? window.location.search : '', - viewport_height: isBrowser ? window.innerHeight : 0, - viewport_width: isBrowser ? window.innerWidth : 0, - }, - }, - headers: { Version: '2' }, - credentials: 'include', - }).catch((e) => { - console.error('Problem sending telemetry page:', e) - }) - }, [pathname, hasAcceptedConsent]) - - const sendPageLeaveTelemetry = useCallback(() => { - if (!(IS_PLATFORM && hasAcceptedConsent)) return - - const title = typeof document !== 'undefined' ? document?.title : '' - - unauthedAllowedPost('/platform/telemetry/page-leave', { - body: { - pathname, - page_url: isBrowser ? window.location.href : '', - page_title: title, - }, - credentials: 'include', - }).catch((e) => { - console.error('Problem sending telemetry page-leave:', e) - }) - }, [pathname, hasAcceptedConsent]) - - return { sendPageTelemetry, sendPageLeaveTelemetry } -} +import { API_URL } from '~/lib/constants' const PageTelemetry = () => { - const pathname = usePathname() - - const title = typeof document !== 'undefined' ? document?.title : '' - const referrer = typeof document !== 'undefined' ? document?.referrer : '' const { hasAcceptedConsent } = useConsent() - useTelemetryCookie({ hasAcceptedConsent, title, referrer }) - const { sendPageTelemetry, sendPageLeaveTelemetry } = useTelemetryWithConsent() - - useEffect(() => { - if (pathname) sendPageTelemetry() - }, [pathname, sendPageTelemetry]) - - useEffect(() => { - if (!pathname) return - - const handleBeforeUnload = () => sendPageLeaveTelemetry() - window.addEventListener('beforeunload', handleBeforeUnload) - return () => window.removeEventListener('beforeunload', handleBeforeUnload) - }, [pathname, sendPageLeaveTelemetry]) - - return null + return ( + + ) } export { PageTelemetry } diff --git a/apps/docs/lib/constants.ts b/apps/docs/lib/constants.ts index fd52e23ad1d..8b44aaff023 100644 --- a/apps/docs/lib/constants.ts +++ b/apps/docs/lib/constants.ts @@ -1,13 +1,11 @@ export const API_URL = ( process.env.NODE_ENV === 'development' ? process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080' - : process.env.NEXT_PUBLIC_API_URL + : process.env.NEXT_PUBLIC_API_URL! )?.replace(/\/platform$/, '') export const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/docs' export const IS_DEV = process.env.NODE_ENV === 'development' export const IS_PLATFORM = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' -export const IS_PREVIEW = process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' export const IS_PRODUCTION = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' -export const LOCAL_SUPABASE = process.env.NEXT_PUBLIC_LOCAL_SUPABASE === 'true' export const MISC_URL = process.env.NEXT_PUBLIC_MISC_URL ?? '' export const PROD_URL = `https://supabase.com${BASE_PATH}` diff --git a/apps/docs/lib/docs.ts b/apps/docs/lib/docs.ts index ca21bd2102f..bfcaf3772a1 100644 --- a/apps/docs/lib/docs.ts +++ b/apps/docs/lib/docs.ts @@ -15,7 +15,7 @@ import codeHikeTheme from 'config/code-hike.theme.json' with { type: 'json' } // with outputFileTracingIncludes (not auto-traced) will not be found at // runtime. export const DOCS_DIRECTORY = process.cwd() -export const EXAMPLES_DIRECTORY = join(DOCS_DIRECTORY, '..', '..', 'examples') +export const EXAMPLES_DIRECTORY = join(DOCS_DIRECTORY, 'examples') export const GUIDES_DIRECTORY = join(DOCS_DIRECTORY, 'content/guides') export const REF_DOCS_DIRECTORY = join(DOCS_DIRECTORY, 'docs/ref') export const SPEC_DIRECTORY = join(DOCS_DIRECTORY, 'spec') diff --git a/apps/docs/lib/fetch/fetchWrappers.ts b/apps/docs/lib/fetch/fetchWrappers.ts index d978d4b06f2..f8a145f5878 100644 --- a/apps/docs/lib/fetch/fetchWrappers.ts +++ b/apps/docs/lib/fetch/fetchWrappers.ts @@ -1,6 +1,7 @@ import type { paths } from 'api-types' import createClient from 'openapi-fetch' import { v4 as uuidv4 } from 'uuid' + import { API_URL } from '../constants' import { getAccessToken } from '../userAuth' @@ -9,16 +10,13 @@ const DEFAULT_HEADERS = { Accept: 'application/json', } -const { GET: _get, POST: _post } = createClient({ +const client = createClient({ baseUrl: API_URL, referrerPolicy: 'no-referrer-when-downgrade', headers: DEFAULT_HEADERS, }) -export async function constructHeaders( - headersInit?: HeadersInit | undefined, - { allowUnauthenticated = false }: { allowUnauthenticated?: boolean } = {} -) { +async function constructHeaders(headersInit?: HeadersInit | undefined) { const requestId = uuidv4() const headers = new Headers(headersInit) @@ -28,39 +26,21 @@ export async function constructHeaders( const accessToken = await getAccessToken() if (accessToken) { headers.set('Authorization', `Bearer ${accessToken}`) - } else if (!allowUnauthenticated) { - throw Error("can't fetch authenticated routes without signing in") } } return headers } -export const get: typeof _get = async (url, init) => { - const headers = await constructHeaders(init?.headers) +client.use({ + async onRequest({ request }) { + const headers = await constructHeaders(request.headers) + headers.forEach((value, key) => { + request.headers.set(key, value) + }) - return await _get(url, { - ...init, - headers, - }) -} + return request + }, +}) -export const post: typeof _post = async (url, init) => { - const headers = await constructHeaders(init?.headers) - - return await _post(url, { - credentials: 'include', - ...init, - headers, - }) -} - -export const unauthedAllowedPost: typeof _post = async (url, init) => { - const headers = await constructHeaders(init?.headers, { allowUnauthenticated: true }) - - return await _post(url, { - credentials: 'include', - ...init, - headers, - }) -} +export const { GET: get, POST: post } = client diff --git a/apps/docs/lib/octokit.ts b/apps/docs/lib/octokit.ts index 155e6f20720..7f18c36e327 100644 --- a/apps/docs/lib/octokit.ts +++ b/apps/docs/lib/octokit.ts @@ -30,7 +30,7 @@ function octokit() { return octokitInstance } -export async function getGitHubFileContents({ +async function getGitHubFileContents({ org, repo, path, diff --git a/apps/docs/lib/telemetry.ts b/apps/docs/lib/telemetry.ts index bfc8b89a73e..e9dd341af3a 100644 --- a/apps/docs/lib/telemetry.ts +++ b/apps/docs/lib/telemetry.ts @@ -1,57 +1,16 @@ -import { isBrowser } from 'common' +import { sendTelemetryEvent } from 'common' +import { TelemetryEvent } from 'common/telemetry-constants' import { usePathname } from 'next/navigation' -import { useConsent } from 'ui-patterns/ConsentToast' -import { unauthedAllowedPost } from './fetch/fetchWrappers' +import { useCallback } from 'react' +import { API_URL } from './constants' -type TelemetryEvent = { - action: string - category: string - label: string -} - -const noop = () => {} - -/** - * Sends a telemetry event to Logflare for tracking by the product team. - * - * Checks for user consent to telemetry before sending. - */ -const useSendTelemetryEvent = () => { +export function useSendTelemetryEvent() { const pathname = usePathname() - const { hasAcceptedConsent } = useConsent() - if (!hasAcceptedConsent) return noop - - const title = typeof document !== 'undefined' ? document?.title : '' - const referrer = typeof document !== 'undefined' ? document?.referrer : '' - - return (event: TelemetryEvent) => - unauthedAllowedPost('/platform/telemetry/event', { - body: { - pathname, - action: event.action, - page_url: isBrowser ? window.location.href : '', - page_title: title, - ph: { - referrer, - language: navigator.language ?? 'en-US', - user_agent: navigator.userAgent, - search: isBrowser ? window.location.search : '', - viewport_height: isBrowser ? window.innerHeight : 0, - viewport_width: isBrowser ? window.innerWidth : 0, - }, - custom_properties: { - category: event.category, - label: event.label, - } as any, - }, - headers: { Version: '2' }, - credentials: 'include', - }) - .then(({ error }) => { - if (error) console.error(error) - }) - .catch((error) => console.error(error)) + return useCallback( + (event: TelemetryEvent) => { + return sendTelemetryEvent(API_URL, event, pathname) + }, + [pathname] + ) } - -export { useSendTelemetryEvent } diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index d2b12e7ea74..3fbd6234301 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -57,7 +57,11 @@ const nextConfig = { experimental: { outputFileTracingIncludes: { '/api/crawlers': ['./features/docs/generated/**/*', './docs/ref/**/*'], - '/guides/**/*': ['./content/guides/**/*', './content/troubleshooting/**/*'], + '/guides/**/*': [ + './content/guides/**/*', + './content/troubleshooting/**/*', + './examples/**/*', + ], '/reference/**/*': ['./features/docs/generated/**/*', './docs/ref/**/*'], }, serverComponentsExternalPackages: ['libpg-query'], diff --git a/apps/docs/package.json b/apps/docs/package.json index eaf3f8679fb..784b3103692 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -4,10 +4,10 @@ "private": true, "scripts": { "preinstall": "npx only-allow pnpm", - "predev": "pnpm run codegen:references", - "dev": "next dev --port 3001", + "predev": "pnpm run codegen:references && pnpm run codegen:examples", + "dev": "concurrently --kill-others \"next dev --port 3001\" \"pnpm run dev:watch:troubleshooting\"", + "dev:watch:troubleshooting": "node ./scripts/troubleshooting/watch.mjs", "dev:secrets:pull": "AWS_PROFILE=supabase-dev node ../../scripts/getSecrets.js -n local/docs", - "prebuild": "pnpm run codegen:references", "build": "next build", "build:analyze": "ANALYZE=true next build", "build:sitemap": "tsx ./internals/generate-sitemap.ts", @@ -16,6 +16,7 @@ "lint": "next lint", "lint:mdx": "supa-mdx-lint content/guides --config ../../supa-mdx-lint.config.toml", "typecheck": "tsc --noEmit", + "pretest": "pnpm run codegen:examples", "test": "vitest --exclude \"**/*.smoke.test.ts\"", "test:smoke": "pnpm run codegen:references && vitest -t \"prod smoke test\"", "embeddings": "tsx scripts/search/generate-embeddings.ts", @@ -23,9 +24,10 @@ "troubleshooting:sync": "node features/docs/Troubleshooting.script.mjs", "last-changed": "tsx scripts/last-changed.ts", "last-changed:reset": "pnpm run last-changed -- --reset", + "codegen:examples": "cp -r ../../examples ./examples", "codegen:references": "tsx features/docs/Reference.generated.script.ts", "codemod:frontmatter": "node ./scripts/codemod/mdx-meta.mjs && prettier --write \"content/**/*.mdx\"", - "clean": "rimraf node_modules ./docs/features/docs/generated" + "clean": "rimraf node_modules features/docs/generated examples" }, "dependencies": { "@code-hike/mdx": "^0.9.0", @@ -40,7 +42,7 @@ "@octokit/plugin-paginate-graphql": "^4.0.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-collapsible": "^1.0.3", - "@supabase/supabase-js": "^2.44.3", + "@supabase/supabase-js": "catalog:", "@tailwindcss/container-queries": "^0.1.1", "@tanstack/react-query": "^5.13.4", "acorn": "^8.11.3", @@ -59,7 +61,7 @@ "isbot": "^5.1.2", "js-yaml": "^3.14.1", "jsrsasign": "^11.0.0", - "katex": "^0.16.10", + "katex": "^0.16.21", "libpg-query": "15.2.0", "lodash": "^4.17.21", "lucide-react": "*", @@ -81,7 +83,7 @@ "next-themes": "^0.3.0", "nuqs": "^1.19.1", "openai": "^4.20.1", - "openapi-fetch": "^0.8.2", + "openapi-fetch": "0.12.4", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", @@ -121,6 +123,8 @@ "@types/unist": "^2.0.6", "api-types": "workspace:*", "cheerio": "^1.0.0-rc.12", + "chokidar": "^4.0.3", + "concurrently": "^9.1.2", "config": "workspace:*", "dotenv": "^16.0.3", "ejs": "^3.1.10", @@ -135,6 +139,6 @@ "typescript": "~5.5.0", "unist-util-visit-parents": "5.1.3", "vite-tsconfig-paths": "^4.3.2", - "vitest": "^2.0.2" + "vitest": "^3.0.0" } } diff --git a/apps/docs/public/img/guides/auth-google/auth-google-consent-screen.png b/apps/docs/public/img/guides/auth-google/auth-google-consent-screen.png new file mode 100644 index 00000000000..76942f3b74d Binary files /dev/null and b/apps/docs/public/img/guides/auth-google/auth-google-consent-screen.png differ diff --git a/apps/docs/public/img/troubleshooting/0e7f6842-78fd-44f9-b463-425507815fb6.png b/apps/docs/public/img/troubleshooting/0e7f6842-78fd-44f9-b463-425507815fb6.png new file mode 100644 index 00000000000..b75cf3c3b3a Binary files /dev/null and b/apps/docs/public/img/troubleshooting/0e7f6842-78fd-44f9-b463-425507815fb6.png differ diff --git a/apps/docs/public/img/troubleshooting/10950be3-264b-4a41-bc71-3d431c2756b2.png b/apps/docs/public/img/troubleshooting/10950be3-264b-4a41-bc71-3d431c2756b2.png new file mode 100644 index 00000000000..6b62f61329f Binary files /dev/null and b/apps/docs/public/img/troubleshooting/10950be3-264b-4a41-bc71-3d431c2756b2.png differ diff --git a/apps/docs/public/img/troubleshooting/152d65ad-f0ed-47cf-8dcb-1e31c6221e71.png b/apps/docs/public/img/troubleshooting/152d65ad-f0ed-47cf-8dcb-1e31c6221e71.png new file mode 100644 index 00000000000..348f6b063a1 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/152d65ad-f0ed-47cf-8dcb-1e31c6221e71.png differ diff --git a/apps/docs/public/img/troubleshooting/18ed2c88-332e-4e66-b9b4-c37e99a39104.png b/apps/docs/public/img/troubleshooting/18ed2c88-332e-4e66-b9b4-c37e99a39104.png new file mode 100644 index 00000000000..274d1fd2604 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/18ed2c88-332e-4e66-b9b4-c37e99a39104.png differ diff --git a/apps/docs/public/img/troubleshooting/224379580-fa77bd31-bb58-47e6-90ce-64e140f32579.png b/apps/docs/public/img/troubleshooting/224379580-fa77bd31-bb58-47e6-90ce-64e140f32579.png new file mode 100644 index 00000000000..f236805dbbc Binary files /dev/null and b/apps/docs/public/img/troubleshooting/224379580-fa77bd31-bb58-47e6-90ce-64e140f32579.png differ diff --git a/apps/docs/public/img/troubleshooting/3eeb2435-dd1c-41bc-9557-44cabff38f59.png b/apps/docs/public/img/troubleshooting/3eeb2435-dd1c-41bc-9557-44cabff38f59.png new file mode 100644 index 00000000000..6e99baedca4 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/3eeb2435-dd1c-41bc-9557-44cabff38f59.png differ diff --git a/apps/docs/public/img/troubleshooting/46f59019-6ff1-4bfb-9acd-d5b7bd282f89.png b/apps/docs/public/img/troubleshooting/46f59019-6ff1-4bfb-9acd-d5b7bd282f89.png new file mode 100644 index 00000000000..66c1a0b5a53 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/46f59019-6ff1-4bfb-9acd-d5b7bd282f89.png differ diff --git a/apps/docs/public/img/troubleshooting/47685206-7914-440e-a010-da62f5c38186.png b/apps/docs/public/img/troubleshooting/47685206-7914-440e-a010-da62f5c38186.png new file mode 100644 index 00000000000..ffa3f78d0bf Binary files /dev/null and b/apps/docs/public/img/troubleshooting/47685206-7914-440e-a010-da62f5c38186.png differ diff --git a/apps/docs/public/img/troubleshooting/47998bed-0b77-433a-bfed-63222beb2aee.png b/apps/docs/public/img/troubleshooting/47998bed-0b77-433a-bfed-63222beb2aee.png new file mode 100644 index 00000000000..2eb18732d1d Binary files /dev/null and b/apps/docs/public/img/troubleshooting/47998bed-0b77-433a-bfed-63222beb2aee.png differ diff --git a/apps/docs/public/img/troubleshooting/49e11154-296b-42b9-8036-8a7288e49b8a.png b/apps/docs/public/img/troubleshooting/49e11154-296b-42b9-8036-8a7288e49b8a.png new file mode 100644 index 00000000000..d56aedf13e5 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/49e11154-296b-42b9-8036-8a7288e49b8a.png differ diff --git a/apps/docs/public/img/troubleshooting/49e175d9-6338-4b17-97be-4bf7853e319f.png b/apps/docs/public/img/troubleshooting/49e175d9-6338-4b17-97be-4bf7853e319f.png new file mode 100644 index 00000000000..cf96e93ce90 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/49e175d9-6338-4b17-97be-4bf7853e319f.png differ diff --git a/apps/docs/public/img/troubleshooting/565c96e0-cbb9-4e7a-b0aa-5b423afc0ada.png b/apps/docs/public/img/troubleshooting/565c96e0-cbb9-4e7a-b0aa-5b423afc0ada.png new file mode 100644 index 00000000000..2b0270cbfef Binary files /dev/null and b/apps/docs/public/img/troubleshooting/565c96e0-cbb9-4e7a-b0aa-5b423afc0ada.png differ diff --git a/apps/docs/public/img/troubleshooting/5b55ddbc-abfc-4c66-a012-b8e4d23cd438.png b/apps/docs/public/img/troubleshooting/5b55ddbc-abfc-4c66-a012-b8e4d23cd438.png new file mode 100644 index 00000000000..f8742c40f46 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/5b55ddbc-abfc-4c66-a012-b8e4d23cd438.png differ diff --git a/apps/docs/public/img/troubleshooting/6c284180-0ffd-432d-b86b-e9fbcfe23868.png b/apps/docs/public/img/troubleshooting/6c284180-0ffd-432d-b86b-e9fbcfe23868.png new file mode 100644 index 00000000000..ddba69bc5b2 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/6c284180-0ffd-432d-b86b-e9fbcfe23868.png differ diff --git a/apps/docs/public/img/troubleshooting/7fe991ac-40ea-4628-a5e3-181d706225ea.png b/apps/docs/public/img/troubleshooting/7fe991ac-40ea-4628-a5e3-181d706225ea.png new file mode 100644 index 00000000000..8b04c2bf89a Binary files /dev/null and b/apps/docs/public/img/troubleshooting/7fe991ac-40ea-4628-a5e3-181d706225ea.png differ diff --git a/apps/docs/public/img/troubleshooting/800448a1-0245-4a26-855f-3be8f44adf03.png b/apps/docs/public/img/troubleshooting/800448a1-0245-4a26-855f-3be8f44adf03.png new file mode 100644 index 00000000000..2296a7fa06a Binary files /dev/null and b/apps/docs/public/img/troubleshooting/800448a1-0245-4a26-855f-3be8f44adf03.png differ diff --git a/apps/docs/public/img/troubleshooting/830b81aa-ae52-44a4-aa49-32c2a6296af7.png b/apps/docs/public/img/troubleshooting/830b81aa-ae52-44a4-aa49-32c2a6296af7.png new file mode 100644 index 00000000000..626d84d8fce Binary files /dev/null and b/apps/docs/public/img/troubleshooting/830b81aa-ae52-44a4-aa49-32c2a6296af7.png differ diff --git a/apps/docs/public/img/troubleshooting/8ba54441-2057-4dc0-8add-a8eaab9f2d93.png b/apps/docs/public/img/troubleshooting/8ba54441-2057-4dc0-8add-a8eaab9f2d93.png new file mode 100644 index 00000000000..bcede723b46 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/8ba54441-2057-4dc0-8add-a8eaab9f2d93.png differ diff --git a/apps/docs/public/img/troubleshooting/90cb6a70-fc67-4666-9217-6fa37cfacf82.png b/apps/docs/public/img/troubleshooting/90cb6a70-fc67-4666-9217-6fa37cfacf82.png new file mode 100644 index 00000000000..b7f8c7c5950 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/90cb6a70-fc67-4666-9217-6fa37cfacf82.png differ diff --git a/apps/docs/public/img/troubleshooting/9b88a738-65d6-4be4-8fa0-bc3f13a9a408.png b/apps/docs/public/img/troubleshooting/9b88a738-65d6-4be4-8fa0-bc3f13a9a408.png new file mode 100644 index 00000000000..e8600fb6064 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/9b88a738-65d6-4be4-8fa0-bc3f13a9a408.png differ diff --git a/apps/docs/public/img/troubleshooting/9c6e0f9c-f842-463b-b214-1a9e31de3b2c.png b/apps/docs/public/img/troubleshooting/9c6e0f9c-f842-463b-b214-1a9e31de3b2c.png new file mode 100644 index 00000000000..033951c6a4f Binary files /dev/null and b/apps/docs/public/img/troubleshooting/9c6e0f9c-f842-463b-b214-1a9e31de3b2c.png differ diff --git a/apps/docs/public/img/troubleshooting/a360b417-e0cc-4706-8df4-89af63dcdc70.png b/apps/docs/public/img/troubleshooting/a360b417-e0cc-4706-8df4-89af63dcdc70.png new file mode 100644 index 00000000000..c8a0bebbf7a Binary files /dev/null and b/apps/docs/public/img/troubleshooting/a360b417-e0cc-4706-8df4-89af63dcdc70.png differ diff --git a/apps/docs/public/img/troubleshooting/ade24c85-50b5-47ca-a2ce-c3f77a0e128a.png b/apps/docs/public/img/troubleshooting/ade24c85-50b5-47ca-a2ce-c3f77a0e128a.png new file mode 100644 index 00000000000..7c7accbc092 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/ade24c85-50b5-47ca-a2ce-c3f77a0e128a.png differ diff --git a/apps/docs/public/img/troubleshooting/c6e228b8-fa64-47a9-bdf6-f1f6405b9cf9.png b/apps/docs/public/img/troubleshooting/c6e228b8-fa64-47a9-bdf6-f1f6405b9cf9.png new file mode 100644 index 00000000000..cd27655527a Binary files /dev/null and b/apps/docs/public/img/troubleshooting/c6e228b8-fa64-47a9-bdf6-f1f6405b9cf9.png differ diff --git a/apps/docs/public/img/troubleshooting/c7ac0f69-6fb9-4ff2-8aab-5cda6bada3f6.png b/apps/docs/public/img/troubleshooting/c7ac0f69-6fb9-4ff2-8aab-5cda6bada3f6.png new file mode 100644 index 00000000000..11ea0ee5608 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/c7ac0f69-6fb9-4ff2-8aab-5cda6bada3f6.png differ diff --git a/apps/docs/public/img/troubleshooting/d756aeb0-515f-425d-b737-75a935935b73.png b/apps/docs/public/img/troubleshooting/d756aeb0-515f-425d-b737-75a935935b73.png new file mode 100644 index 00000000000..10fb585b240 Binary files /dev/null and b/apps/docs/public/img/troubleshooting/d756aeb0-515f-425d-b737-75a935935b73.png differ diff --git a/apps/docs/public/img/troubleshooting/e7d78109-b09d-48ca-8cc6-e4913693e163.png b/apps/docs/public/img/troubleshooting/e7d78109-b09d-48ca-8cc6-e4913693e163.png new file mode 100644 index 00000000000..b75cf3c3b3a Binary files /dev/null and b/apps/docs/public/img/troubleshooting/e7d78109-b09d-48ca-8cc6-e4913693e163.png differ diff --git a/apps/docs/public/img/troubleshooting/ec5ed538-985b-411d-a0d7-be718b28d367.png b/apps/docs/public/img/troubleshooting/ec5ed538-985b-411d-a0d7-be718b28d367.png new file mode 100644 index 00000000000..4273e62922a Binary files /dev/null and b/apps/docs/public/img/troubleshooting/ec5ed538-985b-411d-a0d7-be718b28d367.png differ diff --git a/apps/docs/scripts/search/sources/index.ts b/apps/docs/scripts/search/sources/index.ts index d4110d95afb..0b7f08558a0 100644 --- a/apps/docs/scripts/search/sources/index.ts +++ b/apps/docs/scripts/search/sources/index.ts @@ -1,3 +1,4 @@ +import { sep } from 'node:path' import { GitHubDiscussionLoader, type GitHubDiscussionSource, @@ -93,9 +94,13 @@ export async function fetchSources() { 'spec/common-cli-sections.json' ).load() - const guideSources = (await walk('content')) - .filter(({ path }) => /\.mdx?$/.test(path)) - .filter(({ path }) => !ignoredFiles.includes(path)) + const guideSources = (await walk('content/guides')) + .filter( + ({ path }) => + /\.mdx?$/.test(path) && + !ignoredFiles.includes(path) && + !path.split(sep).some((part) => part.startsWith('_')) + ) .map((entry) => new MarkdownLoader('guide', entry.path, { yaml: true }).load()) const partnerIntegrationSources = (await fetchPartners()).map((partner) => diff --git a/apps/docs/scripts/troubleshooting/watch.mjs b/apps/docs/scripts/troubleshooting/watch.mjs new file mode 100644 index 00000000000..304954500bf --- /dev/null +++ b/apps/docs/scripts/troubleshooting/watch.mjs @@ -0,0 +1,61 @@ +// @ts-check + +import chokidar from 'chokidar' +import { closeSync, openSync, readFileSync, writeSync } from 'node:fs' +import { basename, join } from 'node:path' + +const TROUBLESHOOTING_DIRECTORY = join(import.meta.dirname, '../../content/troubleshooting') + +let __template = '' + +function getTemplate() { + if (!__template) { + __template = readFileSync(join(TROUBLESHOOTING_DIRECTORY, '_template.mdx'), 'utf8') + } + return __template +} + +const watcher = chokidar + .watch(TROUBLESHOOTING_DIRECTORY, { ignoreInitial: true }) + .on('add', (path, stats) => { + if (!stats?.isFile() || basename(path).startsWith('_')) return + + console.log(`File ${path} has been added.`) + + if (stats?.size !== 0) { + console.log(`File ${path} already has content. Skipping template writing.`) + return + } + + try { + console.log(`Writing template to ${path}...`) + const fd = openSync(path, 'w') + writeSync(fd, getTemplate()) + closeSync(fd) + } catch (err) { + console.error(`Error writing template to ${path}:`, err) + } + }) + +// Keep the process alive +const interval = setInterval(() => {}, 1000) + +const shutdown = () => { + console.log(`\nShutting down watcher...`) + clearInterval(interval) + watcher + .close() + .then(() => { + console.log('Watcher closed. Exiting process.') + process.exit(0) + }) + .catch((err) => { + console.error('Error closing watcher:', err) + process.exit(1) + }) +} + +process.on('SIGINT', shutdown) +process.on('SIGTERM', shutdown) +process.on('SIGHUP', shutdown) +process.on('SIGQUIT', shutdown) diff --git a/apps/docs/spec/api_v1_openapi.json b/apps/docs/spec/api_v1_openapi.json index 5ecb7b3a1a1..b1de4affd6c 100644 --- a/apps/docs/spec/api_v1_openapi.json +++ b/apps/docs/spec/api_v1_openapi.json @@ -2061,6 +2061,95 @@ "security": [{ "bearer": [] }] } }, + "/v1/projects/{ref}/restore": { + "get": { + "operationId": "v1-list-available-restore-versions", + "summary": "Lists available restore versions for the given project", + "parameters": [ + { "name": "ref", "required": true, "in": "path", "schema": { "type": "string" } } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetProjectAvailableRestoreVersionsResponse" + } + } + } + }, + "403": { "description": "" } + }, + "tags": ["Projects"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "v1-restore-a-project", + "summary": "Restores the given project", + "parameters": [ + { "name": "ref", "required": true, "in": "path", "schema": { "type": "string" } } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RestoreProjectBodyDto" } + } + } + }, + "responses": { "200": { "description": "" }, "403": { "description": "" } }, + "tags": ["Projects"], + "security": [{ "bearer": [] }] + } + }, + "/v1/projects/{ref}/restore/cancel": { + "post": { + "operationId": "v1-cancel-a-project-restoration", + "summary": "Cancels the given project restoration", + "parameters": [ + { "name": "ref", "required": true, "in": "path", "schema": { "type": "string" } } + ], + "responses": { "200": { "description": "" }, "403": { "description": "" } }, + "tags": ["Projects"], + "security": [{ "bearer": [] }] + } + }, + "/v1/projects/{ref}/analytics/endpoints/logs.all": { + "get": { + "operationId": "getLogs", + "summary": "Gets project's logs", + "parameters": [ + { + "name": "iso_timestamp_end", + "required": false, + "in": "query", + "schema": { "type": "string" } + }, + { + "name": "iso_timestamp_start", + "required": false, + "in": "query", + "schema": { "type": "string" } + }, + { "name": "sql", "required": false, "in": "query", "schema": { "type": "string" } }, + { "name": "ref", "required": true, "in": "path", "schema": { "type": "string" } } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/V1AnalyticsResponse" } + } + } + }, + "403": { "description": "" } + }, + "tags": ["Analytics"], + "security": [{ "bearer": [] }] + } + }, "/v1/projects/{ref}/database/query": { "post": { "operationId": "v1-run-a-query", @@ -2114,7 +2203,62 @@ "security": [{ "bearer": [] }] } }, + "/v1/projects/{ref}/database/context": { + "get": { + "operationId": "getDatabaseMetadata", + "summary": "Gets database metadata for the given project.", + "description": "This is an **experimental** endpoint. It is subject to change or removal in future versions. Use it with caution, as it may not remain supported or stable.", + "deprecated": true, + "parameters": [ + { "name": "ref", "required": true, "in": "path", "schema": { "type": "string" } } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetProjectDbMetadataResponseDto" } + } + } + }, + "403": { "description": "" } + }, + "tags": ["Database"], + "security": [{ "bearer": [] }] + } + }, "/v1/projects/{ref}/functions": { + "get": { + "operationId": "v1-list-all-functions", + "summary": "List all functions", + "description": "Returns all functions you've previously added to the specified project.", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { "minLength": 20, "maxLength": 20, "type": "string" } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/FunctionResponse" } + } + } + } + }, + "403": { "description": "" }, + "500": { "description": "Failed to retrieve project's functions" } + }, + "tags": ["Edge Functions"], + "security": [{ "bearer": [] }] + }, "post": { "operationId": "v1-create-a-function", "summary": "Create a function", @@ -2188,37 +2332,6 @@ }, "tags": ["Edge Functions"], "security": [{ "bearer": [] }] - }, - "get": { - "operationId": "v1-list-all-functions", - "summary": "List all functions", - "description": "Returns all functions you've previously added to the specified project.", - "parameters": [ - { - "name": "ref", - "required": true, - "in": "path", - "description": "Project ref", - "schema": { "minLength": 20, "maxLength": 20, "type": "string" } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/FunctionResponse" } - } - } - } - }, - "403": { "description": "" }, - "500": { "description": "Failed to retrieve project's functions" } - }, - "tags": ["Edge Functions"], - "security": [{ "bearer": [] }] } }, "/v1/projects/{ref}/functions/{function_slug}": { @@ -2711,43 +2824,43 @@ "BranchDetailResponse": { "type": "object", "properties": { + "status": { + "type": "string", + "enum": [ + "INACTIVE", + "ACTIVE_HEALTHY", + "ACTIVE_UNHEALTHY", + "COMING_UP", + "UNKNOWN", + "GOING_DOWN", + "INIT_FAILED", + "REMOVED", + "RESTORING", + "UPGRADING", + "PAUSING", + "RESTORE_FAILED", + "RESTARTING", + "PAUSE_FAILED", + "RESIZING" + ] + }, "db_port": { "type": "integer" }, "ref": { "type": "string" }, "postgres_version": { "type": "string" }, "postgres_engine": { "type": "string" }, "release_channel": { "type": "string" }, - "status": { - "enum": [ - "ACTIVE_HEALTHY", - "ACTIVE_UNHEALTHY", - "COMING_UP", - "GOING_DOWN", - "INACTIVE", - "INIT_FAILED", - "REMOVED", - "RESTARTING", - "UNKNOWN", - "UPGRADING", - "PAUSING", - "RESTORING", - "RESTORE_FAILED", - "PAUSE_FAILED", - "RESIZING" - ], - "type": "string" - }, "db_host": { "type": "string" }, "db_user": { "type": "string" }, "db_pass": { "type": "string" }, "jwt_secret": { "type": "string" } }, "required": [ + "status", "db_port", "ref", "postgres_version", "postgres_engine", "release_channel", - "status", "db_host" ] }, @@ -2763,6 +2876,7 @@ "git_branch": { "type": "string" }, "persistent": { "type": "boolean" }, "status": { + "type": "string", "enum": [ "CREATING_PROJECT", "RUNNING_MIGRATIONS", @@ -2770,8 +2884,7 @@ "MIGRATIONS_FAILED", "FUNCTIONS_DEPLOYED", "FUNCTIONS_FAILED" - ], - "type": "string" + ] } } }, @@ -2784,14 +2897,8 @@ "deprecated": true, "description": "This field is deprecated and will not be populated." }, - "id": { "type": "string" }, - "name": { "type": "string" }, - "project_ref": { "type": "string" }, - "parent_project_ref": { "type": "string" }, - "is_default": { "type": "boolean" }, - "git_branch": { "type": "string" }, - "persistent": { "type": "boolean" }, "status": { + "type": "string", "enum": [ "CREATING_PROJECT", "RUNNING_MIGRATIONS", @@ -2799,20 +2906,26 @@ "MIGRATIONS_FAILED", "FUNCTIONS_DEPLOYED", "FUNCTIONS_FAILED" - ], - "type": "string" + ] }, + "id": { "type": "string" }, + "name": { "type": "string" }, + "project_ref": { "type": "string" }, + "parent_project_ref": { "type": "string" }, + "is_default": { "type": "boolean" }, + "git_branch": { "type": "string" }, + "persistent": { "type": "boolean" }, "created_at": { "type": "string" }, "updated_at": { "type": "string" } }, "required": [ + "status", "id", "name", "project_ref", "parent_project_ref", "is_default", "persistent", - "status", "created_at", "updated_at" ] @@ -2853,29 +2966,29 @@ "description": "Creation timestamp", "example": "2023-03-29T16:32:59Z" }, - "database": { "$ref": "#/components/schemas/V1DatabaseResponse" }, "status": { + "type": "string", "enum": [ + "INACTIVE", "ACTIVE_HEALTHY", "ACTIVE_UNHEALTHY", "COMING_UP", + "UNKNOWN", "GOING_DOWN", - "INACTIVE", "INIT_FAILED", "REMOVED", - "RESTARTING", - "UNKNOWN", + "RESTORING", "UPGRADING", "PAUSING", - "RESTORING", "RESTORE_FAILED", + "RESTARTING", "PAUSE_FAILED", "RESIZING" - ], - "type": "string" - } + ] + }, + "database": { "$ref": "#/components/schemas/V1DatabaseResponse" } }, - "required": ["id", "organization_id", "name", "region", "created_at", "database", "status"] + "required": ["id", "organization_id", "name", "region", "created_at", "status", "database"] }, "V1CreateProjectBodyDto": { "type": "object", @@ -2970,24 +3083,24 @@ "example": "2023-03-29T16:32:59Z" }, "status": { + "type": "string", "enum": [ + "INACTIVE", "ACTIVE_HEALTHY", "ACTIVE_UNHEALTHY", "COMING_UP", + "UNKNOWN", "GOING_DOWN", - "INACTIVE", "INIT_FAILED", "REMOVED", - "RESTARTING", - "UNKNOWN", + "RESTORING", "UPGRADING", "PAUSING", - "RESTORING", "RESTORE_FAILED", + "RESTARTING", "PAUSE_FAILED", "RESIZING" - ], - "type": "string" + ] } }, "required": ["id", "organization_id", "name", "region", "created_at", "status"] @@ -3061,7 +3174,7 @@ "type": { "type": "string", "enum": ["sql"] }, "visibility": { "type": "string", "enum": ["user", "project", "org", "public"] }, "name": { "type": "string" }, - "description": { "type": "string" }, + "description": { "type": "string", "nullable": true }, "project": { "$ref": "#/components/schemas/SnippetProject" }, "owner": { "$ref": "#/components/schemas/SnippetUser" }, "updated_by": { "$ref": "#/components/schemas/SnippetUser" } @@ -3073,6 +3186,7 @@ "type", "visibility", "name", + "description", "project", "owner", "updated_by" @@ -3104,7 +3218,7 @@ "type": { "type": "string", "enum": ["sql"] }, "visibility": { "enum": ["user", "project", "org", "public"], "type": "string" }, "name": { "type": "string" }, - "description": { "type": "string" }, + "description": { "type": "string", "nullable": true }, "project": { "$ref": "#/components/schemas/SnippetProject" }, "owner": { "$ref": "#/components/schemas/SnippetUser" }, "updated_by": { "$ref": "#/components/schemas/SnippetUser" }, @@ -3117,6 +3231,7 @@ "type", "visibility", "name", + "description", "project", "owner", "updated_by", @@ -3627,14 +3742,20 @@ "properties": { "enabled": { "type": "boolean" } }, "required": ["enabled"] }, + "StorageFeatureS3Protocol": { + "type": "object", + "properties": { "enabled": { "type": "boolean" } }, + "required": ["enabled"] + }, "StorageFeatures": { "type": "object", "properties": { "imageTransformation": { "$ref": "#/components/schemas/StorageFeatureImageTransformation" - } + }, + "s3Protocol": { "$ref": "#/components/schemas/StorageFeatureS3Protocol" } }, - "required": ["imageTransformation"] + "required": ["imageTransformation", "s3Protocol"] }, "StorageConfigResponse": { "type": "object", @@ -3662,6 +3783,7 @@ "effective_cache_size": { "type": "string" }, "logical_decoding_work_mem": { "type": "string" }, "maintenance_work_mem": { "type": "string" }, + "track_activity_query_size": { "type": "string" }, "max_connections": { "type": "integer", "minimum": 1, "maximum": 262143 }, "max_locks_per_transaction": { "type": "integer", "minimum": 10, "maximum": 2147483640 }, "max_parallel_maintenance_workers": { "type": "integer", "minimum": 0, "maximum": 1024 }, @@ -3689,6 +3811,7 @@ "effective_cache_size": { "type": "string" }, "logical_decoding_work_mem": { "type": "string" }, "maintenance_work_mem": { "type": "string" }, + "track_activity_query_size": { "type": "string" }, "max_connections": { "type": "integer", "minimum": 1, "maximum": 262143 }, "max_locks_per_transaction": { "type": "integer", "minimum": 10, "maximum": 2147483640 }, "max_parallel_maintenance_workers": { "type": "integer", "minimum": 0, "maximum": 1024 }, @@ -3724,11 +3847,11 @@ "SupavisorConfigResponse": { "type": "object", "properties": { + "database_type": { "type": "string", "enum": ["PRIMARY", "READ_REPLICA"] }, "db_port": { "type": "integer" }, "default_pool_size": { "type": "integer", "nullable": true }, "max_client_conn": { "type": "integer", "nullable": true }, "identifier": { "type": "string" }, - "database_type": { "enum": ["PRIMARY", "READ_REPLICA"], "type": "string" }, "is_using_scram_auth": { "type": "boolean" }, "db_user": { "type": "string" }, "db_host": { "type": "string" }, @@ -3737,11 +3860,11 @@ "pool_mode": { "enum": ["transaction", "session"], "type": "string" } }, "required": [ + "database_type", "db_port", "default_pool_size", "max_client_conn", "identifier", - "database_type", "is_using_scram_auth", "db_user", "db_host", @@ -4337,21 +4460,87 @@ }, "required": ["id", "type", "inserted_at", "updated_at"] }, + "ProjectAvailableRestoreVersion": { + "type": "object", + "properties": { + "version": { "type": "string" }, + "release_channel": { + "type": "string", + "enum": ["internal", "alpha", "beta", "ga", "withdrawn", "preview"] + }, + "postgres_engine": { "type": "string", "enum": ["13", "14", "15", "17-oriole"] } + }, + "required": ["version", "release_channel", "postgres_engine"] + }, + "GetProjectAvailableRestoreVersionsResponse": { + "type": "object", + "properties": { + "available_versions": { + "type": "array", + "items": { "$ref": "#/components/schemas/ProjectAvailableRestoreVersion" } + } + }, + "required": ["available_versions"] + }, + "RestoreProjectBodyDto": { + "type": "object", + "properties": { + "release_channel": { + "type": "string", + "enum": ["internal", "alpha", "beta", "ga", "withdrawn", "preview"], + "description": "Release channel version. If not provided, GeneralAvailability will be used." + }, + "postgres_engine": { + "type": "string", + "enum": ["15", "17-oriole"], + "description": "Postgres engine version. If not provided, the latest version from the given release channel will be used." + } + } + }, + "V1AnalyticsResponse": { + "type": "object", + "properties": { + "error": { + "oneOf": [ + { + "properties": { + "code": { "type": "number" }, + "errors": { + "type": "array", + "items": { + "properties": { + "domain": { "type": "string" }, + "location": { "type": "string" }, + "locationType": { "type": "string" }, + "message": { "type": "string" }, + "reason": { "type": "string" } + } + } + }, + "message": { "type": "string" }, + "status": { "type": "string" } + } + }, + { "type": "string" } + ] + }, + "result": { "type": "array", "items": { "type": "object" } } + } + }, "V1RunQueryBody": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] }, - "V1CreateFunctionBody": { + "GetProjectDbMetadataResponseDto": { "type": "object", "properties": { - "slug": { "type": "string", "pattern": "/^[A-Za-z0-9_-]+$/" }, - "name": { "type": "string" }, - "body": { "type": "string" }, - "verify_jwt": { "type": "boolean" }, - "compute_multiplier": { "type": "number", "minimum": 1, "maximum": 4 } + "databases": { + "type": "array", + "items": { "type": "object", "properties": {}, "additionalProperties": true } + } }, - "required": ["slug", "name", "body"] + "required": ["databases"] }, "FunctionResponse": { "type": "object", @@ -4371,6 +4560,17 @@ }, "required": ["version", "created_at", "updated_at", "id", "slug", "name", "status"] }, + "V1CreateFunctionBody": { + "type": "object", + "properties": { + "slug": { "type": "string", "pattern": "/^[A-Za-z0-9_-]+$/" }, + "name": { "type": "string" }, + "body": { "type": "string" }, + "verify_jwt": { "type": "boolean" }, + "compute_multiplier": { "type": "number", "minimum": 1, "maximum": 4 } + }, + "required": ["slug", "name", "body"] + }, "FunctionSlugResponse": { "type": "object", "properties": { diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index 90d76c6144e..5381236c3dc 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 2.2.1 + version: 2.6.8 title: Supabase CLI language: sh source: https://github.com/supabase/cli @@ -1120,12 +1120,15 @@ commands: subcommands: - supabase-postgres-config-get - supabase-postgres-config-update + - supabase-postgres-config-delete flags: [] - id: supabase-postgres-config-update title: supabase postgres-config update summary: Update Postgres database config description: |- Overriding the default Postgres config could result in unstable database behavior. + If you experience instability after changing configurations, you can revert to the default values + using the 'postgres-config delete' command. Custom configuration also overrides the optimizations generated based on the compute add-ons in use. tags: [] links: [] @@ -1154,6 +1157,32 @@ commands: name: --project-ref description: Project ref of the Supabase project. default_value: '' + - id: supabase-postgres-config-delete + title: supabase postgres-config delete + summary: Delete Postgres database config overrides + description: Delete specific config overrides, reverting them to their default values. + tags: [] + links: [] + usage: supabase postgres-config delete [flags] + subcommands: [] + flags: + - id: config + name: --config + description: Config keys to delete (comma-separated) + default_value: '[]' + - id: no-restart + name: --no-restart + description: Do not restart the database after deleting config. + default_value: 'false' + - id: experimental + name: --experimental + description: enable experimental features + required: true + default_value: 'false' + - id: project-ref + name: --project-ref + description: Project ref of the Supabase project. + default_value: '' - id: supabase-postgres-config-get title: supabase postgres-config get summary: Get the current Postgres database config overrides @@ -2901,9 +2930,13 @@ commands: summary: Starts local Postgres database tags: [] links: [] - usage: supabase db start + usage: supabase db start [flags] subcommands: [] - flags: [] + flags: + - id: from-backup + name: --from-backup + description: Path to a logical backup file. + default_value: '' - id: supabase-db-reset title: supabase db reset summary: Resets the local database to current migrations @@ -3599,9 +3632,26 @@ commands: description: Retrieve details of the specified preview branch. tags: [] links: [] - usage: supabase branches get [branch-id] + usage: supabase branches get [branch-id] [flags] subcommands: [] flags: + - id: output + name: -o, --output <[ pretty | json | toml | yaml ]> + description: Output format of branch details. + default_value: pretty + accepted_values: + - id: pretty + name: pretty + type: '[ pretty | json | toml | yaml ]' + - id: json + name: json + type: '[ pretty | json | toml | yaml ]' + - id: toml + name: toml + type: '[ pretty | json | toml | yaml ]' + - id: yaml + name: yaml + type: '[ pretty | json | toml | yaml ]' - id: experimental name: --experimental description: enable experimental features diff --git a/apps/docs/spec/cli_v1_config.yaml b/apps/docs/spec/cli_v1_config.yaml index fca79cb6203..857b222f7d3 100644 --- a/apps/docs/spec/cli_v1_config.yaml +++ b/apps/docs/spec/cli_v1_config.yaml @@ -385,6 +385,17 @@ parameters: - name: 'PostgreSQL configuration' link: 'https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT' + - id: 'db.settings.track_activity_query_size' + title: 'db.settings.track_activity_query_size' + tags: ['database'] + required: false + description: | + Sets the maximum size of the query string that will be tracked in pg_stat_activity.current_query field. + Note: Changing this parameter requires a database restart. + links: + - name: 'PostgreSQL configuration' + link: 'https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-ACTIVITY-QUERY-SIZE' + - id: 'db.settings.track_commit_timestamp' title: 'db.settings.track_commit_timestamp' tags: ['database'] @@ -1497,6 +1508,21 @@ parameters: [functions.my_function] entrypoint = "path/to/custom/function.ts" + - id: 'functions.function_name.static_files' + title: 'functions..static_files' + tags: ['edge-functions'] + required: false + description: | + Specify an array of static files to be bundled with the function. Supports glob patterns. + + NOTE: only file paths within `functions` directory are supported at the moment. + links: + - name: '`supabase functions` CLI subcommands' + link: 'https://supabase.com/docs/reference/cli/supabase-functions' + usage: | + [functions.my_function] + static_files = [ "./functions/MY_FUNCTION_NAME/*.html", "./functions/MY_FUNCTION_NAME/custom.wasm" ] + - id: 'analytics.enabled' title: 'analytics.enabled' tags: ['analytics'] diff --git a/apps/docs/spec/common-api-sections.json b/apps/docs/spec/common-api-sections.json index 821e282f141..5c82c7615df 100644 --- a/apps/docs/spec/common-api-sections.json +++ b/apps/docs/spec/common-api-sections.json @@ -375,6 +375,12 @@ "type": "category", "title": "Projects", "items": [ + { + "id": "v1-cancel-a-project-restoration", + "title": "Cancel a project restoration", + "slug": "v1-cancel-a-project-restoration", + "type": "operation" + }, { "id": "v1-create-a-project", "title": "Create a project", @@ -435,12 +441,24 @@ "slug": "v1-list-all-projects", "type": "operation" }, + { + "id": "v1-list-available-restore-versions", + "title": "List available restore versions", + "slug": "v1-list-available-restore-versions", + "type": "operation" + }, { "id": "v1-pause-a-project", "title": "Pause a project", "slug": "v1-pause-a-project", "type": "operation" }, + { + "id": "v1-restore-a-project", + "title": "Restore a project", + "slug": "v1-restore-a-project", + "type": "operation" + }, { "id": "v1-update-network-restrictions", "title": "Update network restrictions", diff --git a/apps/docs/spec/common-cli-sections.json b/apps/docs/spec/common-cli-sections.json index 6e2aaff44be..351a0d9d4a0 100644 --- a/apps/docs/spec/common-cli-sections.json +++ b/apps/docs/spec/common-cli-sections.json @@ -840,6 +840,12 @@ "title": "Update Postgres configurations", "slug": "supabase-postgres-config-update", "type": "cli-command" + }, + { + "id": "supabase-postgres-config-delete", + "title": "Delete Postgres configurations", + "slug": "supabase-postgres-config-delete", + "type": "cli-command" } ] }, diff --git a/apps/docs/spec/examples/examples.yml b/apps/docs/spec/examples/examples.yml index 77249cc8fa8..ace8173ecb1 100644 --- a/apps/docs/spec/examples/examples.yml +++ b/apps/docs/spec/examples/examples.yml @@ -4555,6 +4555,64 @@ functions: hideCodeBlock: true + - id: order-parent-table-by-a-referenced-table + name: Order parent table by a referenced table + code: | + ```ts + const { data, error } = await supabase + .from('cities') + .select(` + name, + country:countries ( + name + ) + `) + .order('country(name)', { ascending: true }) + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 1, 'Munich'), + (2, 1, 'Bali'); + ``` + response: | + ```json + { + "data": [ + { + "name": "Munich", + "country": { "name": "Germany" } + }, + { + "name": "Bali", + "country": { "name": "Indonesia" } + } + ], + "status": 200, + "statusText": "OK" + } + ``` + description: | + Ordering with `referenced_table(col)` affects the ordering of the + parent table. + hideCodeBlock: true - id: limit() $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.limit' diff --git a/apps/docs/spec/supabase_csharp_v1.yml b/apps/docs/spec/supabase_csharp_v1.yml index 35ba174d3b1..f67e453007d 100644 --- a/apps/docs/spec/supabase_csharp_v1.yml +++ b/apps/docs/spec/supabase_csharp_v1.yml @@ -1190,6 +1190,15 @@ functions: .Order("cities", "name", Ordering.Descending) .Get(); ``` + - id: order-parent-table-by-a-referenced-table + name: Order parent table by a referenced table + code: | + ```c# + var result = await supabase.From() + .Select("name, country:countries(name)") + .Order("country(name)", Ordering.Ascending) + .Get(); + ``` - id: range title: Range() diff --git a/apps/docs/spec/supabase_dart_v2.yml b/apps/docs/spec/supabase_dart_v2.yml index adbcca2ddec..98073255510 100644 --- a/apps/docs/spec/supabase_dart_v2.yml +++ b/apps/docs/spec/supabase_dart_v2.yml @@ -4683,6 +4683,56 @@ functions: } ] ``` + - id: order-parent-table-by-a-referenced-table + name: Order parent table by a referenced table + code: | + ```dart + final data = await supabase + .from('cities') + .select(''' + name, + country:countries ( + name + ) + ''') + .order('country(name)', ascending: true) + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 1, 'Munich'), + (2, 2, 'Bali'); + ``` + response: | + ```json + [ + { + "name": "Munich", + "country": { "name": "Germany" } + }, + { + "name": "Bali", + "country": { "name": "Indonesia" } + } + ] + ``` - id: limit title: limit() description: | diff --git a/apps/docs/spec/supabase_js_v2.yml b/apps/docs/spec/supabase_js_v2.yml index 294008954d6..eeeb94d5674 100644 --- a/apps/docs/spec/supabase_js_v2.yml +++ b/apps/docs/spec/supabase_js_v2.yml @@ -6155,8 +6155,66 @@ functions: } ``` description: | - Ordering on referenced tables doesn't affect the ordering of - the parent table. + Ordering with `referencedTable` doesn't affect the ordering of the + parent table. + hideCodeBlock: true + - id: order-parent-table-by-a-referenced-table + name: Order parent table by a referenced table + code: | + ```ts + const { data, error } = await supabase + .from('cities') + .select(` + name, + country:countries ( + name + ) + `) + .order('country(name)', { ascending: true }) + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 1, 'Munich'), + (2, 2, 'Bali'); + ``` + response: | + ```json + { + "data": [ + { + "name": "Munich", + "country": { "name": "Germany" } + }, + { + "name": "Bali", + "country": { "name": "Indonesia" } + } + ], + "status": 200, + "statusText": "OK" + } + ``` + description: | + Ordering with `referenced_table(col)` affects the ordering of the + parent table. hideCodeBlock: true - id: limit diff --git a/apps/docs/spec/supabase_kt_v3.yml b/apps/docs/spec/supabase_kt_v3.yml index 9689c733da2..1ae14fe113c 100644 --- a/apps/docs/spec/supabase_kt_v3.yml +++ b/apps/docs/spec/supabase_kt_v3.yml @@ -2507,6 +2507,64 @@ functions: Ordering on foreign tables doesn't affect the ordering of the parent table. hideCodeBlock: true + - id: order-parent-table-by-a-referenced-table + name: Order parent table by a referenced table + code: | + ```kotlin + val columns = Columns.raw(""" + name, + country:countries(name) + """.trimIndent()) + supabase.from("cities").select( + columns = columns + ) { + order(column = "country(name)", order = Order.ASCENDING) + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 1, 'Munich'), + (2, 2, 'Bali'); + ``` + response: | + ```json + { + "data": [ + { + "name": "Munich", + "country": { "name": "Germany" } + }, + { + "name": "Bali", + "country": { "name": "Indonesia" } + } + ], + "status": 200, + "statusText": "OK" + } + ``` + description: | + Ordering with `referenced_table(col)` affects the ordering of the + parent table. + hideCodeBlock: true - id: limit title: limit() diff --git a/apps/docs/spec/supabase_py_v2.yml b/apps/docs/spec/supabase_py_v2.yml index 25a1d45ff29..ba8abf41f28 100644 --- a/apps/docs/spec/supabase_py_v2.yml +++ b/apps/docs/spec/supabase_py_v2.yml @@ -5909,6 +5909,60 @@ functions: Ordering on foreign tables doesn't affect the ordering of the parent table. hideCodeBlock: true + - id: order-parent-table-by-a-referenced-table + name: Order parent table by a referenced table + code: | + ```python + response = ( + supabase.table("cities") + .select("name, country:countries(name)") + .order("country(name)", desc=False) + ) + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 1, 'Munich'), + (2, 2, 'Bali'); + ``` + response: | + ```json + { + "data": [ + { + "name": "Munich", + "country": { "name": "Germany" } + }, + { + "name": "Bali", + "country": { "name": "Indonesia" } + } + ], + "status": 200, + "statusText": "OK" + } + ``` + description: | + Ordering with `referenced_table(col)` affects the ordering of the + parent table. + hideCodeBlock: true - id: limit title: limit() params: diff --git a/apps/docs/spec/supabase_swift_v2.yml b/apps/docs/spec/supabase_swift_v2.yml index e63548c8f33..21d0436cef2 100644 --- a/apps/docs/spec/supabase_swift_v2.yml +++ b/apps/docs/spec/supabase_swift_v2.yml @@ -32,7 +32,7 @@ functions: code: | ```swift let client = SupabaseClient( - supabaseURL: URL(string: "https://xyzcompany.supabase.co")!, + supabaseURL: URL(string: "https://xyzcompany.supabase.co")!, supabaseKey: "public-anon-key", options: SupabaseClientOptions( db: .init( @@ -62,7 +62,7 @@ functions: schema: "other_schema" ) ) - ) + ) ``` description: | By default the API server points to the `public` schema. You can enable other database schemas within the Dashboard. @@ -136,7 +136,7 @@ functions: code: | ```swift let supabase = SupabaseClient( - supabaseURL: URL(string: "https://xyzcompany.supabase.co")!, + supabaseURL: URL(string: "https://xyzcompany.supabase.co")!, supabaseKey: "public-anon-key", options: .init( auth: .init( @@ -256,7 +256,7 @@ functions: - `INITIAL_SESSION` - Emitted right after the Supabase client is constructed and the initial session from storage is loaded. - `SIGNED_IN` - - Emitted each time a user session is confirmed or re-established, including on user sign in. + - Emitted each time a user session is confirmed or re-established, including on user sign in. - Avoid making assumptions as to when this event is fired, this may occur even when the user is already signed in. Instead, check the user object attached to the event to see if a new user has signed in and update your application's UI. - `SIGNED_OUT` - Emitted when the user signs out. This can be after: @@ -291,7 +291,7 @@ functions: } // Using Closure - let subscription = await supabase.auth.onAuthStateChange { event, session in + let subscription = await supabase.auth.onAuthStateChange { event, session in print(event, session) } @@ -498,7 +498,7 @@ functions: ```swift let session = try await supabase.auth.signInWithOAuth( provider: .github - ) { (session: ASWebAuthenticationSession) in + ) { (session: ASWebAuthenticationSession) in // customize session } ``` @@ -512,7 +512,7 @@ functions: ```swift let session = try await supabase.auth.signInWithOAuth( provider: .github - ) { url in + ) { url in // use url to start OAuth flow // and return a result url that contains the OAuth token. // ... @@ -531,7 +531,7 @@ functions: ```swift let url = try await supabase.auth.getOAuthSignInURL(provider: .github, redirectTo: URL(string: "my-app-scheme://")) - let session = ASWebAuthenticationSession(url: url, callbackURLScheme: "my-app-scheme") { url, error in + let session = ASWebAuthenticationSession(url: url, callbackURLScheme: "my-app-scheme") { url, error in guard let url else { return } Task { @@ -899,7 +899,7 @@ functions: isSpotlight: true code: | ```swift - try await supabase.auth.linkIdentity(provider: provider) { url in + try await supabase.auth.linkIdentity(provider: provider) { url in // custom URL opening logic } ``` @@ -919,7 +919,7 @@ functions: // retrieve all identites linked to a user let identities = try await supabase.auth.userIdentities() - // find the google identity + // find the google identity let googleIdentity = identities.first { $0.provider == .google } @@ -1397,7 +1397,7 @@ functions: let name: String let teams: [Team] } - + struct Team: Decodable { let name: String } @@ -1936,7 +1936,7 @@ functions: .insert(Country(id: 1, name: "Denmark")) .select() // specify you want a single value returned, otherwise it returns a list. - .single() + .single() .execute() .value ``` @@ -2048,7 +2048,7 @@ functions: ```swift struct Country: Decodable { let id: Int - let name: String + let name: String } let country: Country = try await supabase @@ -2825,8 +2825,8 @@ functions: filter() expects you to use the raw PostgREST syntax for the filter values. ```swift - .filter("id", operator: .in, value: "(5,6,7)") // Use `()` for `in` filter - .filter("arraycol", operator: .cs, value: #"{"a","b"}"#) // Use `cs` for `contains()`, `{}` for array values + .filter("id", operator: "in", value: "(5,6,7)") // Use `()` for `in` filter + .filter("arraycol", operator: "cs", value: #"{"a","b"}"#) // Use `cs` for `contains()`, `{}` for array values ``` examples: - id: with-select @@ -2836,7 +2836,7 @@ functions: try await supabase .from("countries") .select() - .filter("name", operator: .in, value: #"("Algeria","Japan")"#) + .filter("name", operator: "in", value: #"("Algeria","Japan")"#) ``` data: sql: | @@ -2880,7 +2880,7 @@ functions: ) """ ) - .filter("cities.name", operator: .eq, value: "Bali") + .filter("cities.name", operator: "eq", value: "Bali") ``` data: sql: | @@ -3095,6 +3095,66 @@ functions: Ordering on foreign tables doesn't affect the ordering of the parent table. hideCodeBlock: true + - id: order-parent-table-by-a-referenced-table + name: Order parent table by a referenced table + code: | + ```swift + try await supabase + .from("cities") + .select( + """ + name, + country:countries ( + name + ) + """ + ) + .order("country(name)", ascending: true) + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 1, 'Munich'), + (2, 2, 'Bali'); + ``` + response: | + ```swifton + { + "data": [ + { + "name": "Munich", + "country": { "name": "Germany" } + }, + { + "name": "Bali", + "country": { "name": "Indonesia" } + } + ], + "status": 200, + "statusText": "OK" + } + ``` + description: | + Ordering with `referenced_table(col)` affects the ordering of the + parent table. + hideCodeBlock: true - id: limit title: limit() @@ -3435,7 +3495,7 @@ functions: let response: Response = try await supabase.functions .invoke( - "hello", + "hello", options: FunctionInvokeOptions( body: ["foo": "bar"] ) @@ -3448,11 +3508,11 @@ functions: ```swift let response = try await supabase.functions .invoke( - "hello", + "hello", options: FunctionInvokeOptions( body: ["foo": "bar"] ), - decode: { data, response in + decode: { data, response in String(data: data, encoding: .utf8) } ) @@ -3484,7 +3544,7 @@ functions: do { let response = try await supabase.functions .invoke( - "hello", + "hello", options: FunctionInvokeOptions( body: ["foo": "bar"] ) @@ -3506,7 +3566,7 @@ functions: ```swift let response = try await supabase.functions .invoke( - "hello", + "hello", options: FunctionInvokeOptions( headers: [ "my-custom-header": "my-custom-header-value" @@ -3554,7 +3614,7 @@ functions: ```swift let response = try await supabase.functions .invoke( - "hello", + "hello", options: FunctionInvokeOptions( method: .delete, headers: [ @@ -3573,7 +3633,7 @@ functions: ```swift let response = try await supabase.functions .invoke( - "hello", + "hello", options: FunctionInvokeOptions( method: .get, headers: [ @@ -3752,10 +3812,10 @@ functions: name: Listen to a specific table code: | ```swift - let channel = supabase.channel("channelId") + let channel = supabase.channel("channelId") let changeStream = channel.postgresChange( - AnyAction.self, + AnyAction.self, schema: "public", table: "users" ) @@ -3817,13 +3877,13 @@ functions: name: Listen to inserts using callback code: | ```swift - let channel = supabase.channel("channelId") + let channel = supabase.channel("channelId") let subscription = channel.onPostgresChange( - InsertAction.self, + InsertAction.self, schema: "public", table: "users" - ) { insert in + ) { insert in print("Inserted: \(insert.record)") } @@ -3843,16 +3903,16 @@ functions: ``` code: | ```swift - let channel = supabase.channel("channelId") - + let channel = supabase.channel("channelId") + let updates = channel.postgresChange( - UpdateAction.self, + UpdateAction.self, schema: "public", table: "users" ) await channel.subscribe() - + for await update in updates { print("Updated: \(update.oldRecord) with \(update.record)") } @@ -3868,18 +3928,18 @@ functions: ``` code: | ```swift - let channel = supabase.channel("channelId") - + let channel = supabase.channel("channelId") + let subscription = channel.onPostgresChange( - UpdateAction.self, + UpdateAction.self, schema: "public", table: "users" - ) { update in + ) { update in print("Updated: \(update.oldRecord) with \(update.record)") } await channel.subscribe() - + // remove subscription some time later subscription.cancel() ``` @@ -3894,10 +3954,10 @@ functions: ``` code: | ```swift - let channel = supabase.channel("channelId") + let channel = supabase.channel("channelId") let deletions = channel.postgresChange( - DeleteAction.self, + DeleteAction.self, schema: "public", table: "users" ) @@ -3919,10 +3979,10 @@ functions: ``` code: | ```swift - let channel = supabase.channel("channelId") + let channel = supabase.channel("channelId") let subscription = channel.onPostgresChange( - DeleteAction.self, + DeleteAction.self, schema: "public", table: "users" ) { deletion in @@ -3938,10 +3998,10 @@ functions: name: Listen to row level changes code: | ```swift - let channel = supabase.channel("channelId") + let channel = supabase.channel("channelId") let deletions = channel.postgresChange( - DeleteAction.self, + DeleteAction.self, schema: "public", table: "users", filter: "id=eq.1" @@ -3957,14 +4017,14 @@ functions: name: Listen to row level changes using callback code: | ```swift - let channel = supabase.channel("channelId") + let channel = supabase.channel("channelId") let subscription = channel.onPostgresChange( - DeleteAction.self, + DeleteAction.self, schema: "public", table: "users", filter: "id=eq.1" - ) { deletion in + ) { deletion in print("Deleted: \(deletion.oldRecord)") } @@ -3988,7 +4048,7 @@ functions: isSpotlight: true code: | ```swift - let channel = supabase.channel("channelId") + let channel = supabase.channel("channelId") //... await supabase.removeChannel(channel) @@ -3998,7 +4058,7 @@ functions: isSpotlight: true code: | ```swift - let channel = supabase.channel("channelId") + let channel = supabase.channel("channelId") //... await channel.unsubscribe() @@ -4081,7 +4141,7 @@ functions: ```swift try await supabase.storage .createBucket( - "avatars", + "avatars", options: BucketOptions( public: false, allowedMimeTypes: ["image/png"], @@ -4121,7 +4181,7 @@ functions: ```swift try await supabase.storage .updateBucket( - "avatars", + "avatars", options: BucketOptions( public: false, fileSizeLimit: 1024, diff --git a/apps/docs/spec/transforms/api_v1_openapi_deparsed.json b/apps/docs/spec/transforms/api_v1_openapi_deparsed.json index 50b2c52dae6..3026c4a5ddd 100644 --- a/apps/docs/spec/transforms/api_v1_openapi_deparsed.json +++ b/apps/docs/spec/transforms/api_v1_openapi_deparsed.json @@ -78,6 +78,26 @@ "schema": { "type": "object", "properties": { + "status": { + "type": "string", + "enum": [ + "INACTIVE", + "ACTIVE_HEALTHY", + "ACTIVE_UNHEALTHY", + "COMING_UP", + "UNKNOWN", + "GOING_DOWN", + "INIT_FAILED", + "REMOVED", + "RESTORING", + "UPGRADING", + "PAUSING", + "RESTORE_FAILED", + "RESTARTING", + "PAUSE_FAILED", + "RESIZING" + ] + }, "db_port": { "type": "integer" }, @@ -93,26 +113,6 @@ "release_channel": { "type": "string" }, - "status": { - "enum": [ - "ACTIVE_HEALTHY", - "ACTIVE_UNHEALTHY", - "COMING_UP", - "GOING_DOWN", - "INACTIVE", - "INIT_FAILED", - "REMOVED", - "RESTARTING", - "UNKNOWN", - "UPGRADING", - "PAUSING", - "RESTORING", - "RESTORE_FAILED", - "PAUSE_FAILED", - "RESIZING" - ], - "type": "string" - }, "db_host": { "type": "string" }, @@ -127,12 +127,12 @@ } }, "required": [ + "status", "db_port", "ref", "postgres_version", "postgres_engine", "release_channel", - "status", "db_host" ] } @@ -187,6 +187,7 @@ "type": "boolean" }, "status": { + "type": "string", "enum": [ "CREATING_PROJECT", "RUNNING_MIGRATIONS", @@ -194,8 +195,7 @@ "MIGRATIONS_FAILED", "FUNCTIONS_DEPLOYED", "FUNCTIONS_FAILED" - ], - "type": "string" + ] } } } @@ -219,6 +219,17 @@ "deprecated": true, "description": "This field is deprecated and will not be populated." }, + "status": { + "type": "string", + "enum": [ + "CREATING_PROJECT", + "RUNNING_MIGRATIONS", + "MIGRATIONS_PASSED", + "MIGRATIONS_FAILED", + "FUNCTIONS_DEPLOYED", + "FUNCTIONS_FAILED" + ] + }, "id": { "type": "string" }, @@ -240,17 +251,6 @@ "persistent": { "type": "boolean" }, - "status": { - "enum": [ - "CREATING_PROJECT", - "RUNNING_MIGRATIONS", - "MIGRATIONS_PASSED", - "MIGRATIONS_FAILED", - "FUNCTIONS_DEPLOYED", - "FUNCTIONS_FAILED" - ], - "type": "string" - }, "created_at": { "type": "string" }, @@ -259,13 +259,13 @@ } }, "required": [ + "status", "id", "name", "project_ref", "parent_project_ref", "is_default", "persistent", - "status", "created_at", "updated_at" ] @@ -462,6 +462,26 @@ "description": "Creation timestamp", "example": "2023-03-29T16:32:59Z" }, + "status": { + "type": "string", + "enum": [ + "INACTIVE", + "ACTIVE_HEALTHY", + "ACTIVE_UNHEALTHY", + "COMING_UP", + "UNKNOWN", + "GOING_DOWN", + "INIT_FAILED", + "REMOVED", + "RESTORING", + "UPGRADING", + "PAUSING", + "RESTORE_FAILED", + "RESTARTING", + "PAUSE_FAILED", + "RESIZING" + ] + }, "database": { "type": "object", "properties": { @@ -483,26 +503,6 @@ } }, "required": ["host", "version", "postgres_engine", "release_channel"] - }, - "status": { - "enum": [ - "ACTIVE_HEALTHY", - "ACTIVE_UNHEALTHY", - "COMING_UP", - "GOING_DOWN", - "INACTIVE", - "INIT_FAILED", - "REMOVED", - "RESTARTING", - "UNKNOWN", - "UPGRADING", - "PAUSING", - "RESTORING", - "RESTORE_FAILED", - "PAUSE_FAILED", - "RESIZING" - ], - "type": "string" } }, "required": [ @@ -511,8 +511,8 @@ "name", "region", "created_at", - "database", - "status" + "status", + "database" ] } } @@ -654,24 +654,24 @@ "example": "2023-03-29T16:32:59Z" }, "status": { + "type": "string", "enum": [ + "INACTIVE", "ACTIVE_HEALTHY", "ACTIVE_UNHEALTHY", "COMING_UP", + "UNKNOWN", "GOING_DOWN", - "INACTIVE", "INIT_FAILED", "REMOVED", - "RESTARTING", - "UNKNOWN", + "RESTORING", "UPGRADING", "PAUSING", - "RESTORING", "RESTORE_FAILED", + "RESTARTING", "PAUSE_FAILED", "RESIZING" - ], - "type": "string" + ] } }, "required": ["id", "organization_id", "name", "region", "created_at", "status"] @@ -1069,7 +1069,8 @@ "type": "string" }, "description": { - "type": "string" + "type": "string", + "nullable": true }, "project": { "type": "object", @@ -1118,6 +1119,7 @@ "type", "visibility", "name", + "description", "project", "owner", "updated_by" @@ -1189,7 +1191,8 @@ "type": "string" }, "description": { - "type": "string" + "type": "string", + "nullable": true }, "project": { "type": "object", @@ -1253,6 +1256,7 @@ "type", "visibility", "name", + "description", "project", "owner", "updated_by", @@ -1885,6 +1889,17 @@ "deprecated": true, "description": "This field is deprecated and will not be populated." }, + "status": { + "type": "string", + "enum": [ + "CREATING_PROJECT", + "RUNNING_MIGRATIONS", + "MIGRATIONS_PASSED", + "MIGRATIONS_FAILED", + "FUNCTIONS_DEPLOYED", + "FUNCTIONS_FAILED" + ] + }, "id": { "type": "string" }, @@ -1906,17 +1921,6 @@ "persistent": { "type": "boolean" }, - "status": { - "enum": [ - "CREATING_PROJECT", - "RUNNING_MIGRATIONS", - "MIGRATIONS_PASSED", - "MIGRATIONS_FAILED", - "FUNCTIONS_DEPLOYED", - "FUNCTIONS_FAILED" - ], - "type": "string" - }, "created_at": { "type": "string" }, @@ -1925,13 +1929,13 @@ } }, "required": [ + "status", "id", "name", "project_ref", "parent_project_ref", "is_default", "persistent", - "status", "created_at", "updated_at" ] @@ -2034,6 +2038,17 @@ "deprecated": true, "description": "This field is deprecated and will not be populated." }, + "status": { + "type": "string", + "enum": [ + "CREATING_PROJECT", + "RUNNING_MIGRATIONS", + "MIGRATIONS_PASSED", + "MIGRATIONS_FAILED", + "FUNCTIONS_DEPLOYED", + "FUNCTIONS_FAILED" + ] + }, "id": { "type": "string" }, @@ -2055,17 +2070,6 @@ "persistent": { "type": "boolean" }, - "status": { - "enum": [ - "CREATING_PROJECT", - "RUNNING_MIGRATIONS", - "MIGRATIONS_PASSED", - "MIGRATIONS_FAILED", - "FUNCTIONS_DEPLOYED", - "FUNCTIONS_FAILED" - ], - "type": "string" - }, "created_at": { "type": "string" }, @@ -2074,13 +2078,13 @@ } }, "required": [ + "status", "id", "name", "project_ref", "parent_project_ref", "is_default", "persistent", - "status", "created_at", "updated_at" ] @@ -3445,6 +3449,26 @@ "description": "Creation timestamp", "example": "2023-03-29T16:32:59Z" }, + "status": { + "type": "string", + "enum": [ + "INACTIVE", + "ACTIVE_HEALTHY", + "ACTIVE_UNHEALTHY", + "COMING_UP", + "UNKNOWN", + "GOING_DOWN", + "INIT_FAILED", + "REMOVED", + "RESTORING", + "UPGRADING", + "PAUSING", + "RESTORE_FAILED", + "RESTARTING", + "PAUSE_FAILED", + "RESIZING" + ] + }, "database": { "type": "object", "properties": { @@ -3466,26 +3490,6 @@ } }, "required": ["host", "version", "postgres_engine", "release_channel"] - }, - "status": { - "enum": [ - "ACTIVE_HEALTHY", - "ACTIVE_UNHEALTHY", - "COMING_UP", - "GOING_DOWN", - "INACTIVE", - "INIT_FAILED", - "REMOVED", - "RESTARTING", - "UNKNOWN", - "UPGRADING", - "PAUSING", - "RESTORING", - "RESTORE_FAILED", - "PAUSE_FAILED", - "RESIZING" - ], - "type": "string" } }, "required": [ @@ -3494,8 +3498,8 @@ "name", "region", "created_at", - "database", - "status" + "status", + "database" ] } } @@ -4801,9 +4805,18 @@ } }, "required": ["enabled"] + }, + "s3Protocol": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": ["enabled"] } }, - "required": ["imageTransformation"] + "required": ["imageTransformation", "s3Protocol"] } }, "required": ["fileSizeLimit", "features"] @@ -4865,9 +4878,18 @@ } }, "required": ["enabled"] + }, + "s3Protocol": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": ["enabled"] } }, - "required": ["imageTransformation"] + "required": ["imageTransformation", "s3Protocol"] } } } @@ -4927,6 +4949,9 @@ "maintenance_work_mem": { "type": "string" }, + "track_activity_query_size": { + "type": "string" + }, "max_connections": { "type": "integer", "minimum": 1, @@ -5045,6 +5070,9 @@ "maintenance_work_mem": { "type": "string" }, + "track_activity_query_size": { + "type": "string" + }, "max_connections": { "type": "integer", "minimum": 1, @@ -5140,6 +5168,9 @@ "maintenance_work_mem": { "type": "string" }, + "track_activity_query_size": { + "type": "string" + }, "max_connections": { "type": "integer", "minimum": 1, @@ -5310,6 +5341,10 @@ "items": { "type": "object", "properties": { + "database_type": { + "type": "string", + "enum": ["PRIMARY", "READ_REPLICA"] + }, "db_port": { "type": "integer" }, @@ -5324,10 +5359,6 @@ "identifier": { "type": "string" }, - "database_type": { - "enum": ["PRIMARY", "READ_REPLICA"], - "type": "string" - }, "is_using_scram_auth": { "type": "boolean" }, @@ -5349,11 +5380,11 @@ } }, "required": [ + "database_type", "db_port", "default_pool_size", "max_client_conn", "identifier", - "database_type", "is_using_scram_auth", "db_user", "db_host", @@ -8176,6 +8207,257 @@ ] } }, + "/v1/projects/{ref}/restore": { + "get": { + "operationId": "v1-list-available-restore-versions", + "summary": "Lists available restore versions for the given project", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "available_versions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "release_channel": { + "type": "string", + "enum": ["internal", "alpha", "beta", "ga", "withdrawn", "preview"] + }, + "postgres_engine": { + "type": "string", + "enum": ["13", "14", "15", "17-oriole"] + } + }, + "required": ["version", "release_channel", "postgres_engine"] + } + } + }, + "required": ["available_versions"] + } + } + } + }, + "403": { + "description": "" + } + }, + "tags": ["Projects"], + "security": [ + { + "bearer": [] + } + ] + }, + "post": { + "operationId": "v1-restore-a-project", + "summary": "Restores the given project", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "release_channel": { + "type": "string", + "enum": ["internal", "alpha", "beta", "ga", "withdrawn", "preview"], + "description": "Release channel version. If not provided, GeneralAvailability will be used." + }, + "postgres_engine": { + "type": "string", + "enum": ["15", "17-oriole"], + "description": "Postgres engine version. If not provided, the latest version from the given release channel will be used." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["Projects"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/v1/projects/{ref}/restore/cancel": { + "post": { + "operationId": "v1-cancel-a-project-restoration", + "summary": "Cancels the given project restoration", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + }, + "403": { + "description": "" + } + }, + "tags": ["Projects"], + "security": [ + { + "bearer": [] + } + ] + } + }, + "/v1/projects/{ref}/analytics/endpoints/logs.all": { + "get": { + "operationId": "getLogs", + "summary": "Gets project's logs", + "parameters": [ + { + "name": "iso_timestamp_end", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "iso_timestamp_start", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "sql", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "ref", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "oneOf": [ + { + "properties": { + "code": { + "type": "number" + }, + "errors": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string" + }, + "location": { + "type": "string" + }, + "locationType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + } + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + { + "type": "string" + } + ] + }, + "result": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + }, + "403": { + "description": "" + } + }, + "tags": ["Analytics"], + "security": [ + { + "bearer": [] + } + ] + } + }, "/v1/projects/{ref}/database/query": { "post": { "operationId": "v1-run-a-query", @@ -8271,7 +8553,152 @@ ] } }, + "/v1/projects/{ref}/database/context": { + "get": { + "operationId": "getDatabaseMetadata", + "summary": "Gets database metadata for the given project.", + "description": "This is an **experimental** endpoint. It is subject to change or removal in future versions. Use it with caution, as it may not remain supported or stable.", + "deprecated": true, + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "databases": { + "type": "array", + "items": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + } + }, + "required": ["databases"] + } + } + } + }, + "403": { + "description": "" + } + }, + "tags": ["Database"], + "security": [ + { + "bearer": [] + } + ] + } + }, "/v1/projects/{ref}/functions": { + "get": { + "operationId": "v1-list-all-functions", + "summary": "List all functions", + "description": "Returns all functions you've previously added to the specified project.", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "version": { + "type": "integer" + }, + "created_at": { + "type": "integer", + "format": "int64" + }, + "updated_at": { + "type": "integer", + "format": "int64" + }, + "id": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "enum": ["ACTIVE", "REMOVED", "THROTTLED"], + "type": "string" + }, + "verify_jwt": { + "type": "boolean" + }, + "import_map": { + "type": "boolean" + }, + "entrypoint_path": { + "type": "string" + }, + "import_map_path": { + "type": "string" + }, + "compute_multiplier": { + "type": "number" + } + }, + "required": [ + "version", + "created_at", + "updated_at", + "id", + "slug", + "name", + "status" + ] + } + } + } + } + }, + "403": { + "description": "" + }, + "500": { + "description": "Failed to retrieve project's functions" + } + }, + "tags": ["Edge Functions"], + "security": [ + { + "bearer": [] + } + ] + }, "post": { "operationId": "v1-create-a-function", "summary": "Create a function", @@ -8479,101 +8906,6 @@ "bearer": [] } ] - }, - "get": { - "operationId": "v1-list-all-functions", - "summary": "List all functions", - "description": "Returns all functions you've previously added to the specified project.", - "parameters": [ - { - "name": "ref", - "required": true, - "in": "path", - "description": "Project ref", - "schema": { - "minLength": 20, - "maxLength": 20, - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "version": { - "type": "integer" - }, - "created_at": { - "type": "integer", - "format": "int64" - }, - "updated_at": { - "type": "integer", - "format": "int64" - }, - "id": { - "type": "string" - }, - "slug": { - "type": "string" - }, - "name": { - "type": "string" - }, - "status": { - "enum": ["ACTIVE", "REMOVED", "THROTTLED"], - "type": "string" - }, - "verify_jwt": { - "type": "boolean" - }, - "import_map": { - "type": "boolean" - }, - "entrypoint_path": { - "type": "string" - }, - "import_map_path": { - "type": "string" - }, - "compute_multiplier": { - "type": "number" - } - }, - "required": [ - "version", - "created_at", - "updated_at", - "id", - "slug", - "name", - "status" - ] - } - } - } - } - }, - "403": { - "description": "" - }, - "500": { - "description": "Failed to retrieve project's functions" - } - }, - "tags": ["Edge Functions"], - "security": [ - { - "bearer": [] - } - ] } }, "/v1/projects/{ref}/functions/{function_slug}": { @@ -10166,6 +10498,26 @@ "BranchDetailResponse": { "type": "object", "properties": { + "status": { + "type": "string", + "enum": [ + "INACTIVE", + "ACTIVE_HEALTHY", + "ACTIVE_UNHEALTHY", + "COMING_UP", + "UNKNOWN", + "GOING_DOWN", + "INIT_FAILED", + "REMOVED", + "RESTORING", + "UPGRADING", + "PAUSING", + "RESTORE_FAILED", + "RESTARTING", + "PAUSE_FAILED", + "RESIZING" + ] + }, "db_port": { "type": "integer" }, @@ -10181,26 +10533,6 @@ "release_channel": { "type": "string" }, - "status": { - "enum": [ - "ACTIVE_HEALTHY", - "ACTIVE_UNHEALTHY", - "COMING_UP", - "GOING_DOWN", - "INACTIVE", - "INIT_FAILED", - "REMOVED", - "RESTARTING", - "UNKNOWN", - "UPGRADING", - "PAUSING", - "RESTORING", - "RESTORE_FAILED", - "PAUSE_FAILED", - "RESIZING" - ], - "type": "string" - }, "db_host": { "type": "string" }, @@ -10215,12 +10547,12 @@ } }, "required": [ + "status", "db_port", "ref", "postgres_version", "postgres_engine", "release_channel", - "status", "db_host" ] }, @@ -10242,6 +10574,7 @@ "type": "boolean" }, "status": { + "type": "string", "enum": [ "CREATING_PROJECT", "RUNNING_MIGRATIONS", @@ -10249,8 +10582,7 @@ "MIGRATIONS_FAILED", "FUNCTIONS_DEPLOYED", "FUNCTIONS_FAILED" - ], - "type": "string" + ] } } }, @@ -10266,6 +10598,17 @@ "deprecated": true, "description": "This field is deprecated and will not be populated." }, + "status": { + "type": "string", + "enum": [ + "CREATING_PROJECT", + "RUNNING_MIGRATIONS", + "MIGRATIONS_PASSED", + "MIGRATIONS_FAILED", + "FUNCTIONS_DEPLOYED", + "FUNCTIONS_FAILED" + ] + }, "id": { "type": "string" }, @@ -10287,17 +10630,6 @@ "persistent": { "type": "boolean" }, - "status": { - "enum": [ - "CREATING_PROJECT", - "RUNNING_MIGRATIONS", - "MIGRATIONS_PASSED", - "MIGRATIONS_FAILED", - "FUNCTIONS_DEPLOYED", - "FUNCTIONS_FAILED" - ], - "type": "string" - }, "created_at": { "type": "string" }, @@ -10306,13 +10638,13 @@ } }, "required": [ + "status", "id", "name", "project_ref", "parent_project_ref", "is_default", "persistent", - "status", "created_at", "updated_at" ] @@ -10385,6 +10717,26 @@ "description": "Creation timestamp", "example": "2023-03-29T16:32:59Z" }, + "status": { + "type": "string", + "enum": [ + "INACTIVE", + "ACTIVE_HEALTHY", + "ACTIVE_UNHEALTHY", + "COMING_UP", + "UNKNOWN", + "GOING_DOWN", + "INIT_FAILED", + "REMOVED", + "RESTORING", + "UPGRADING", + "PAUSING", + "RESTORE_FAILED", + "RESTARTING", + "PAUSE_FAILED", + "RESIZING" + ] + }, "database": { "type": "object", "properties": { @@ -10406,29 +10758,9 @@ } }, "required": ["host", "version", "postgres_engine", "release_channel"] - }, - "status": { - "enum": [ - "ACTIVE_HEALTHY", - "ACTIVE_UNHEALTHY", - "COMING_UP", - "GOING_DOWN", - "INACTIVE", - "INIT_FAILED", - "REMOVED", - "RESTARTING", - "UNKNOWN", - "UPGRADING", - "PAUSING", - "RESTORING", - "RESTORE_FAILED", - "PAUSE_FAILED", - "RESIZING" - ], - "type": "string" } }, - "required": ["id", "organization_id", "name", "region", "created_at", "database", "status"] + "required": ["id", "organization_id", "name", "region", "created_at", "status", "database"] }, "V1CreateProjectBodyDto": { "type": "object", @@ -10541,24 +10873,24 @@ "example": "2023-03-29T16:32:59Z" }, "status": { + "type": "string", "enum": [ + "INACTIVE", "ACTIVE_HEALTHY", "ACTIVE_UNHEALTHY", "COMING_UP", + "UNKNOWN", "GOING_DOWN", - "INACTIVE", "INIT_FAILED", "REMOVED", - "RESTARTING", - "UNKNOWN", + "RESTORING", "UPGRADING", "PAUSING", - "RESTORING", "RESTORE_FAILED", + "RESTARTING", "PAUSE_FAILED", "RESIZING" - ], - "type": "string" + ] } }, "required": ["id", "organization_id", "name", "region", "created_at", "status"] @@ -10700,7 +11032,8 @@ "type": "string" }, "description": { - "type": "string" + "type": "string", + "nullable": true }, "project": { "type": "object", @@ -10749,6 +11082,7 @@ "type", "visibility", "name", + "description", "project", "owner", "updated_by" @@ -10783,7 +11117,8 @@ "type": "string" }, "description": { - "type": "string" + "type": "string", + "nullable": true }, "project": { "type": "object", @@ -10832,6 +11167,7 @@ "type", "visibility", "name", + "description", "project", "owner", "updated_by" @@ -10883,7 +11219,8 @@ "type": "string" }, "description": { - "type": "string" + "type": "string", + "nullable": true }, "project": { "type": "object", @@ -10947,6 +11284,7 @@ "type", "visibility", "name", + "description", "project", "owner", "updated_by", @@ -12175,6 +12513,15 @@ }, "required": ["enabled"] }, + "StorageFeatureS3Protocol": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": ["enabled"] + }, "StorageFeatures": { "type": "object", "properties": { @@ -12186,9 +12533,18 @@ } }, "required": ["enabled"] + }, + "s3Protocol": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": ["enabled"] } }, - "required": ["imageTransformation"] + "required": ["imageTransformation", "s3Protocol"] }, "StorageConfigResponse": { "type": "object", @@ -12208,9 +12564,18 @@ } }, "required": ["enabled"] + }, + "s3Protocol": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": ["enabled"] } }, - "required": ["imageTransformation"] + "required": ["imageTransformation", "s3Protocol"] } }, "required": ["fileSizeLimit", "features"] @@ -12235,9 +12600,18 @@ } }, "required": ["enabled"] + }, + "s3Protocol": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": ["enabled"] } }, - "required": ["imageTransformation"] + "required": ["imageTransformation", "s3Protocol"] } } }, @@ -12253,6 +12627,9 @@ "maintenance_work_mem": { "type": "string" }, + "track_activity_query_size": { + "type": "string" + }, "max_connections": { "type": "integer", "minimum": 1, @@ -12337,6 +12714,9 @@ "maintenance_work_mem": { "type": "string" }, + "track_activity_query_size": { + "type": "string" + }, "max_connections": { "type": "integer", "minimum": 1, @@ -12436,6 +12816,10 @@ "SupavisorConfigResponse": { "type": "object", "properties": { + "database_type": { + "type": "string", + "enum": ["PRIMARY", "READ_REPLICA"] + }, "db_port": { "type": "integer" }, @@ -12450,10 +12834,6 @@ "identifier": { "type": "string" }, - "database_type": { - "enum": ["PRIMARY", "READ_REPLICA"], - "type": "string" - }, "is_using_scram_auth": { "type": "boolean" }, @@ -12475,11 +12855,11 @@ } }, "required": [ + "database_type", "db_port", "default_pool_size", "max_client_conn", "identifier", - "database_type", "is_using_scram_auth", "db_user", "db_host", @@ -13985,6 +14365,117 @@ }, "required": ["id", "type", "inserted_at", "updated_at"] }, + "ProjectAvailableRestoreVersion": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "release_channel": { + "type": "string", + "enum": ["internal", "alpha", "beta", "ga", "withdrawn", "preview"] + }, + "postgres_engine": { + "type": "string", + "enum": ["13", "14", "15", "17-oriole"] + } + }, + "required": ["version", "release_channel", "postgres_engine"] + }, + "GetProjectAvailableRestoreVersionsResponse": { + "type": "object", + "properties": { + "available_versions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "release_channel": { + "type": "string", + "enum": ["internal", "alpha", "beta", "ga", "withdrawn", "preview"] + }, + "postgres_engine": { + "type": "string", + "enum": ["13", "14", "15", "17-oriole"] + } + }, + "required": ["version", "release_channel", "postgres_engine"] + } + } + }, + "required": ["available_versions"] + }, + "RestoreProjectBodyDto": { + "type": "object", + "properties": { + "release_channel": { + "type": "string", + "enum": ["internal", "alpha", "beta", "ga", "withdrawn", "preview"], + "description": "Release channel version. If not provided, GeneralAvailability will be used." + }, + "postgres_engine": { + "type": "string", + "enum": ["15", "17-oriole"], + "description": "Postgres engine version. If not provided, the latest version from the given release channel will be used." + } + } + }, + "V1AnalyticsResponse": { + "type": "object", + "properties": { + "error": { + "oneOf": [ + { + "properties": { + "code": { + "type": "number" + }, + "errors": { + "type": "array", + "items": { + "properties": { + "domain": { + "type": "string" + }, + "location": { + "type": "string" + }, + "locationType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + } + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + { + "type": "string" + } + ] + }, + "result": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, "V1RunQueryBody": { "type": "object", "properties": { @@ -13994,29 +14485,19 @@ }, "required": ["query"] }, - "V1CreateFunctionBody": { + "GetProjectDbMetadataResponseDto": { "type": "object", "properties": { - "slug": { - "type": "string", - "pattern": "/^[A-Za-z0-9_-]+$/" - }, - "name": { - "type": "string" - }, - "body": { - "type": "string" - }, - "verify_jwt": { - "type": "boolean" - }, - "compute_multiplier": { - "type": "number", - "minimum": 1, - "maximum": 4 + "databases": { + "type": "array", + "items": { + "type": "object", + "properties": {}, + "additionalProperties": true + } } }, - "required": ["slug", "name", "body"] + "required": ["databases"] }, "FunctionResponse": { "type": "object", @@ -14063,6 +14544,30 @@ }, "required": ["version", "created_at", "updated_at", "id", "slug", "name", "status"] }, + "V1CreateFunctionBody": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "pattern": "/^[A-Za-z0-9_-]+$/" + }, + "name": { + "type": "string" + }, + "body": { + "type": "string" + }, + "verify_jwt": { + "type": "boolean" + }, + "compute_multiplier": { + "type": "number", + "minimum": 1, + "maximum": 4 + } + }, + "required": ["slug", "name", "body"] + }, "FunctionSlugResponse": { "type": "object", "properties": { diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index d59159b33b5..7c10606df6a 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -35,5 +35,5 @@ ".next/types/**/*.ts", "./../../packages/ui/src/**/*.d.ts" ], - "exclude": ["node_modules"] + "exclude": ["node_modules", "examples"] } diff --git a/apps/docs/turbo.json b/apps/docs/turbo.json new file mode 100644 index 00000000000..e9f478056fe --- /dev/null +++ b/apps/docs/turbo.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "codegen:examples": { + "inputs": ["../../examples/**"], + "outputs": ["examples/**"] + }, + "codegen:references": { + "inputs": ["spec/**"], + "outputs": ["features/docs/generated/**"] + }, + "build": { + "dependsOn": ["^build", "codegen:examples", "codegen:references"], + "env": [ + "ANALYZE", + "NEXT_PUBLIC_SUPABASE_URL", + "NEXT_PUBLIC_SUPABASE_ANON_KEY", + "NEXT_PUBLIC_MISC_URL", + "NEXT_PUBLIC_MISC_ANON_KEY", + "NODE_ENV", + "NEXT_PUBLIC_API_URL", + "NEXT_PUBLIC_BASE_PATH", + "NEXT_PUBLIC_SITE_URL", + "NEXT_PUBLIC_DEV_AUTH_PAGE", + "NEXT_PUBLIC_IS_PLATFORM", + "NEXT_PUBLIC_VERCEL_ENV", + // These envs are used in the packages + "NEXT_PUBLIC_STORAGE_KEY", + "NEXT_PUBLIC_AUTH_DEBUG_KEY", + "NEXT_PUBLIC_AUTH_PERSISTED_KEY", + "NEXT_PUBLIC_AUTH_NAVIGATOR_LOCK_KEY", + "NEXT_PUBLIC_AUTH_DETECT_SESSION_IN_URL", + "NEXT_PUBLIC_GOTRUE_URL", + "NEXT_PUBLIC_SUPABASE_ANON_KEY", + // These envs are technically passthrough env vars because they're only used on the server side of Nextjs + "DOCS_GITHUB_APP_ID", + "DOCS_GITHUB_APP_INSTALLATION_ID", + "DOCS_GITHUB_APP_PRIVATE_KEY", + "DOCS_REVALIDATION_KEYS", + "DOCS_REVALIDATION_OVERRIDE_KEYS", + "SUPABASE_SECRET_KEY", + "OPENAI_API_KEY" + ], + "inputs": ["$TURBO_DEFAULT$"], + "outputs": [".next/**", "!.next/cache/**"] + } + } +} diff --git a/apps/docs/vitest.config.ts b/apps/docs/vitest.config.ts index 0e7ddac448c..b0047198db1 100644 --- a/apps/docs/vitest.config.ts +++ b/apps/docs/vitest.config.ts @@ -2,5 +2,8 @@ import { defineConfig } from 'vitest/config' import tsconfigPaths from 'vite-tsconfig-paths' export default defineConfig({ + test: { + exclude: ['examples/**/*', '**/node_modules/**'], + }, plugins: [tsconfigPaths({ root: import.meta.dirname })], }) diff --git a/apps/studio/components/grid/components/editor/DateTimeEditor.tsx b/apps/studio/components/grid/components/editor/DateTimeEditor.tsx index 1676996991d..129cf6440be 100644 --- a/apps/studio/components/grid/components/editor/DateTimeEditor.tsx +++ b/apps/studio/components/grid/components/editor/DateTimeEditor.tsx @@ -50,6 +50,17 @@ function BaseEditor({ onRowChange({ ...row, [column.key]: value }, true) } + const setToNow = () => { + const formattedNow = dayjs().format( + type === 'date' + ? 'YYYY-MM-DD' + : type === 'datetimetz' + ? 'YYYY-MM-DDTHH:mm:ssZ' + : 'YYYY-MM-DDTHH:mm:ss' + ) + saveChanges(formattedNow) + } + useEffect(() => { try { ref.current?.focus({ preventScroll: true }) @@ -75,7 +86,7 @@ function BaseEditor({ setInputValue(e.target.value)} className="border-0 rounded-none bg-dash-sidebar outline-none !ring-0 !ring-offset-0" /> @@ -123,7 +134,7 @@ function BaseEditor({
- {isNullable && ( + {isNullable ? ( <> )}
diff --git a/apps/studio/components/grid/components/editor/JsonEditor.tsx b/apps/studio/components/grid/components/editor/JsonEditor.tsx index 87b6863db12..b706bb2a32c 100644 --- a/apps/studio/components/grid/components/editor/JsonEditor.tsx +++ b/apps/studio/components/grid/components/editor/JsonEditor.tsx @@ -11,7 +11,7 @@ import { useGetCellValueMutation } from 'data/table-rows/get-cell-value-mutation import { MAX_CHARACTERS } from 'data/table-rows/table-rows-query' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { prettifyJSON, removeJSONTrailingComma, tryParseJson } from 'lib/helpers' -import { Popover, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui' +import { Popover, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { BlockKeys } from '../common/BlockKeys' import { MonacoEditor } from '../common/MonacoEditor' import { NullValue } from '../common/NullValue' @@ -204,8 +204,8 @@ export const JsonEditor = ({ )} - - + +
({ >
-
- + + Expand editor - -
+ + ) diff --git a/apps/studio/components/grid/components/editor/TextEditor.tsx b/apps/studio/components/grid/components/editor/TextEditor.tsx index 16353b0abae..d50ded31439 100644 --- a/apps/studio/components/grid/components/editor/TextEditor.tsx +++ b/apps/studio/components/grid/components/editor/TextEditor.tsx @@ -9,14 +9,7 @@ import { isTableLike } from 'data/table-editor/table-editor-types' import { useGetCellValueMutation } from 'data/table-rows/get-cell-value-mutation' import { MAX_CHARACTERS } from 'data/table-rows/table-rows-query' import { useSelectedProject } from 'hooks/misc/useSelectedProject' -import { - Button, - Popover, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, - Tooltip_Shadcn_, - cn, -} from 'ui' +import { Button, Popover, Tooltip, TooltipContent, TooltipTrigger, cn } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { useTrackedState } from '../../store/Store' import { BlockKeys } from '../common/BlockKeys' @@ -166,17 +159,17 @@ export const TextEditor = ({
- - + +
diff --git a/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx b/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx index 82973ff6987..d5ec8d48b17 100644 --- a/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx +++ b/apps/studio/components/grid/components/formatter/ForeignKeyFormatter.tsx @@ -8,7 +8,7 @@ import { useTableEditorQuery } from 'data/table-editor/table-editor-query' import { isTableLike } from 'data/table-editor/table-editor-types' import { useTablesQuery } from 'data/tables/tables-query' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' -import { Button, Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui' +import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import type { SupaRow } from '../../types' import { NullValue } from '../common/NullValue' @@ -56,8 +56,8 @@ export const ForeignKeyFormatter = (props: Props) => { {value === null ? : value} {relationship !== undefined && targetTable !== undefined && value !== null && ( - - + + - - View referencing record - + + View referencing record + )} ) diff --git a/apps/studio/components/grid/components/grid/ColumnHeader.tsx b/apps/studio/components/grid/components/grid/ColumnHeader.tsx index 92c7cfb152a..ad1011d01c4 100644 --- a/apps/studio/components/grid/components/grid/ColumnHeader.tsx +++ b/apps/studio/components/grid/components/grid/ColumnHeader.tsx @@ -5,7 +5,7 @@ import { useDrag, useDrop } from 'react-dnd' import { getForeignKeyCascadeAction } from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils' import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' -import { Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui' +import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { useDispatch, useTrackedState } from '../../store/Store' import type { ColumnHeaderProps, ColumnType, DragItem, GridForeignKey } from '../../types' import { ColumnMenu } from '../menu' @@ -126,30 +126,30 @@ export function ColumnHeader({
{renderColumnIcon(columnType, { name: column.name as string, foreignKey })} {isPrimaryKey && ( - - + +
-
- + + Primary key - -
+ + )} {column.name} {columnFormat} {isEncrypted && ( - - + + - - + + Encrypted column - - + + )}
@@ -167,11 +167,11 @@ function renderColumnIcon( case 'foreign_key': // [Joshen] Look into this separately but this should be a hover card instead return ( - - + + - - + +

Foreign key relation:

@@ -193,8 +193,8 @@ function renderColumnIcon(

)}
- - + + ) default: return null diff --git a/apps/studio/components/grid/components/grid/SelectColumn.tsx b/apps/studio/components/grid/components/grid/SelectColumn.tsx index 7d5fd336c21..49032ca9e7a 100644 --- a/apps/studio/components/grid/components/grid/SelectColumn.tsx +++ b/apps/studio/components/grid/components/grid/SelectColumn.tsx @@ -1,4 +1,3 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import { Maximize2 } from 'lucide-react' import { ChangeEvent, InputHTMLAttributes, SyntheticEvent, useEffect, useRef } from 'react' import { @@ -9,7 +8,7 @@ import { useRowSelection, } from 'react-data-grid' -import { Button } from 'ui' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { SELECT_COLUMN_KEY } from '../../constants' import { useTrackedState } from '../../store/Store' import type { SupaRow } from '../../types' @@ -139,30 +138,19 @@ function SelectCellFormatter({ onClick={onClick} /> {onEditRow && row && ( - - -
) diff --git a/apps/studio/components/grid/components/menu/ColumnMenu.tsx b/apps/studio/components/grid/components/menu/ColumnMenu.tsx index 0b08b825585..2ae8315e5af 100644 --- a/apps/studio/components/grid/components/menu/ColumnMenu.tsx +++ b/apps/studio/components/grid/components/menu/ColumnMenu.tsx @@ -1,4 +1,3 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import { ChevronDown, Edit, Lock, Trash, Unlock } from 'lucide-react' import type { CalculatedColumn } from 'react-data-grid' @@ -9,6 +8,9 @@ import { DropdownMenuItem, DropdownMenuTrigger, Separator, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' import { useDispatch, useTrackedState } from '../../store/Store' @@ -44,31 +46,17 @@ const ColumnMenu = ({ column, isEncrypted }: ColumnMenuProps) => { return ( <> {state.editable && onEditColumn !== undefined && ( - - + +

Edit column

-
+ {isEncrypted && ( - - - -
- - Encrypted columns cannot be edited - -
-
-
+ Encrypted columns cannot be edited )} -
+ )} { return ( <> -
+
Token, diff --git a/apps/studio/components/interfaces/Account/AuditLogs.tsx b/apps/studio/components/interfaces/Account/AuditLogs.tsx index 85420e473d2..f41e60689ce 100644 --- a/apps/studio/components/interfaces/Account/AuditLogs.tsx +++ b/apps/studio/components/interfaces/Account/AuditLogs.tsx @@ -72,7 +72,7 @@ const AuditLogs = () => { return ( <>
-
+

Filter by

{

No audit logs found based on the filters applied

) : ( -
- Action - , - - Target - , - -
-

Date

+
+
+ Action + , + + Target + , + +
+

Date

+ + ) : ( + + ) + } + onClick={() => setDateSortDesc(!dateSortDesc)} + tooltip={{ + content: { + side: 'bottom', + text: dateSortDesc ? 'Sort latest first' : 'Sort earliest first', + }, + }} + /> +
+
, + , + ]} + body={ + sortedLogs?.map((log) => { + const project = projects?.find( + (project) => project.ref === log.target.metadata.project_ref + ) + const organization = organizations?.find( + (org) => org.slug === log.target.metadata.org_slug + ) - - ) : ( - - ) - } - onClick={() => setDateSortDesc(!dateSortDesc)} - tooltip={{ - content: { - side: 'bottom', - text: dateSortDesc ? 'Sort latest first' : 'Sort earliest first', - }, - }} - /> - - , - , - ]} - body={ - sortedLogs?.map((log) => { - const project = projects?.find( - (project) => project.ref === log.target.metadata.project_ref - ) - const organization = organizations?.find( - (org) => org.slug === log.target.metadata.org_slug - ) + const hasStatusCode = log.action.metadata[0]?.status !== undefined - const hasStatusCode = log.action.metadata[0]?.status !== undefined - - return ( - setSelectedLog(log)} - className="cursor-pointer hover:!bg-alternative transition duration-100" - > - -
- {hasStatusCode && ( -

- {log.action.metadata[0].status} + return ( + setSelectedLog(log)} + className="cursor-pointer hover:!bg-alternative transition duration-100" + > + +

+ {hasStatusCode && ( +

+ {log.action.metadata[0].status} +

+ )} +

+ {log.action.name}

- )} -

- {log.action.name} +

+ + +

+ {project?.name + ? 'Project: ' + : organization?.name + ? 'Organization: ' + : null} + {project?.name ?? organization?.name ?? '-'}

-
-
- -

- {project?.name - ? 'Project: ' - : organization?.name - ? 'Organization: ' - : null} - {project?.name ?? organization?.name ?? '-'} -

-

- {log.target.metadata.project_ref - ? 'Ref: ' - : log.target.metadata.org_slug - ? 'Slug: ' - : null} - {log.target.metadata.project_ref ?? log.target.metadata.org_slug} -

-
- - {dayjs(log.occurred_at).format('DD MMM YYYY, HH:mm:ss')} - - - - -
- ) - }) ?? [] - } - /> +

+ {log.target.metadata.project_ref + ? 'Ref: ' + : log.target.metadata.org_slug + ? 'Slug: ' + : null} + {log.target.metadata.project_ref ?? log.target.metadata.org_slug} +

+ + + {dayjs(log.occurred_at).format('DD MMM YYYY, HH:mm:ss')} + + + + + + ) + }) ?? [] + } + /> + )} )} diff --git a/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx b/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx index 85dfa041e0b..55d88082340 100644 --- a/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx @@ -46,7 +46,7 @@ export const ProfileInformation = ({ profile }: { profile: Profile }) => { return ( <> Profile Information} footer={ {
{ aria-label="Choose a theme" defaultValue={theme} value={theme} - className="flex flex-wrap gap-5" + className="flex flex-wrap gap-2 md:gap-5" > {singleThemes.map((theme: Theme) => ( { Appearance}>
-
+
Theme mode @@ -83,7 +83,7 @@ const ThemeSettings = () => {

-
+

Supabase will use your selected theme

diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx index fe519e25821..ab4c993426d 100644 --- a/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx +++ b/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx @@ -41,7 +41,7 @@ const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps) => { } isLoading={isEnrolling} onSuccess={() => onClose()} onClose={onClose} diff --git a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx index 7a19a898f9a..7ae2be788e9 100644 --- a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx +++ b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx @@ -2,6 +2,7 @@ import { useMonaco } from '@monaco-editor/react' import { useTheme } from 'next-themes' import { PropsWithChildren, useMemo } from 'react' +import { ClockSkewBanner } from 'components/layouts/AppLayout/ClockSkewBanner' import IncidentBanner from 'components/layouts/AppLayout/IncidentBanner' import { NoticeBanner } from 'components/layouts/AppLayout/NoticeBanner' import { RestrictionBanner } from 'components/layouts/AppLayout/RestrictionBanner' @@ -16,6 +17,7 @@ const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => { const ongoingIncident = useFlag('ongoingIncident') const showNoticeBanner = useFlag('showNoticeBanner') + const clockSkewBanner = useFlag('clockSkewBanner') // Define the supabase theme for Monaco before anything is rendered. Using useEffect would sometime load the theme // after the editor was loaded, so it looked off. useMemo will always be run before rendering @@ -32,6 +34,7 @@ const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => { {ongoingIncident && } {showNoticeBanner && } {profile !== undefined && } + {clockSkewBanner && }
{children}
diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index c9c56da1053..bfd8d5a3ada 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -2,17 +2,21 @@ import { ExternalLink, Eye, EyeOff, FlaskConical } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' +import { TelemetryActions } from 'common/telemetry-constants' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useFlag } from 'hooks/ui/useFlag' -import { TelemetryActions } from 'lib/constants/telemetry' import { useAppStateSnapshot } from 'state/app-state' import { Badge, Button, Modal, ScrollArea, cn } from 'ui' import { FEATURE_PREVIEWS, useFeaturePreviewContext } from './FeaturePreviewContext' +import { useParams } from 'common' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' const FeaturePreviewModal = () => { const snap = useAppStateSnapshot() const featurePreviewContext = useFeaturePreviewContext() const { mutate: sendEvent } = useSendEventMutation() + const { ref: projectRef } = useParams() + const org = useSelectedOrganization() const enableFunctionsAssistant = useFlag('functionsAssistantV2') const selectedFeaturePreview = @@ -33,6 +37,7 @@ const FeaturePreviewModal = () => { ? TelemetryActions.FEATURE_PREVIEW_DISABLED : TelemetryActions.FEATURE_PREVIEW_ENABLED, properties: { feature: selectedFeatureKey }, + groups: { project: projectRef ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) } @@ -51,7 +56,10 @@ const FeaturePreviewModal = () => { useEffect(() => { if (snap.showFeaturePreviewModal) { - sendEvent({ action: TelemetryActions.FEATURE_PREVIEWS_CLICKED }) + sendEvent({ + action: TelemetryActions.FEATURE_PREVIEWS_CLICKED, + groups: { project: projectRef ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, + }) } }, [snap.showFeaturePreviewModal]) diff --git a/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm/AdvancedAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm/AdvancedAuthSettingsForm.tsx index c8455d5aa3e..1cd50cd0cbb 100644 --- a/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm/AdvancedAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm/AdvancedAuthSettingsForm.tsx @@ -142,8 +142,6 @@ const AdvancedAuthSettingsForm = () => { authConfig?.HOOK_SEND_SMS_URI !== null && authConfig?.HOOK_SEND_SMS_ENABLED === true const hasValidMFAPhoneProvider = authConfig?.EXTERNAL_PHONE_ENABLED === true const hasValidMFAProvider = hasValidMFAPhoneProvider || sendSMSHookIsEnabled - const phoneMFAIsEnabled = - INITIAL_VALUES.MFA_PHONE === 'Enabled' || INITIAL_VALUES.MFA_PHONE === 'Verify Enabled' const onSubmit = (values: any, { resetForm }: any) => { let payload = { ...values } @@ -214,6 +212,8 @@ const AdvancedAuthSettingsForm = () => { const hasUpgradedPhoneMFA = INITIAL_VALUES.MFA_PHONE === 'Disabled' && values.MFA_PHONE !== INITIAL_VALUES.MFA_PHONE + const phoneMFAIsEnabled = + values.MFA_PHONE === 'Enabled' || values.MFA_PHONE === 'Verify Enabled' return ( <> diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx index 0d35ca6f79a..9f2f3e5f416 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx @@ -205,7 +205,7 @@ const ProviderForm = ({ config, provider }: ProviderFormProps) => { - - - {showTools ? 'Hide' : 'Show'} tools - - + + {showTools ? 'Hide' : 'Show'} tools + ) } diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/RLSCodeEditor.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/RLSCodeEditor.tsx index fdaec522c6b..b36d37dead4 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/RLSCodeEditor.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/RLSCodeEditor.tsx @@ -1,12 +1,12 @@ import Editor, { Monaco, OnChange, OnMount, useMonaco } from '@monaco-editor/react' +import { noop } from 'lodash' import type { editor } from 'monaco-editor' import { MutableRefObject, useEffect, useRef } from 'react' import { cn } from 'ui' import { Markdown } from 'components/interfaces/Markdown' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { formatQuery } from 'data/sql/format-sql-query' -import { noop } from 'lodash' +import { formatSql } from 'lib/formatSql' // [Joshen] Is there a way we can just have one single MonacoEditor component that's shared across the dashboard? // Feels like we're creating multiple copies of Editor. I'm keen to make this one the defacto as well so lets make sure @@ -125,31 +125,17 @@ export const RLSCodeEditor = ({ onChange() }, [value]) - async function formatPgsql(value: any) { - try { - const formatted = await formatQuery({ - projectRef: project?.ref!, - connectionString: project?.connectionString, - sql: value, - }) - return formatted - } catch (error) { - console.error('formatPgsql error:', error) - return value - } - } - useEffect(() => { if (monaco) { // Enable pgsql format const formatprovider = monaco.languages.registerDocumentFormattingEditProvider('pgsql', { async provideDocumentFormattingEdits(model: any) { const value = model.getValue() - const formatted = await formatPgsql(value) + const formatted = formatSql(value) return [ { range: model.getFullModelRange(), - text: formatted.result.trim(), + text: formatted.trim(), }, ] }, diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx index 4ee9c0d21d6..223d54076d2 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/index.tsx @@ -291,7 +291,7 @@ export const PolicyEditorPanel = memo(function ({ size={showTools ? 'lg' : 'default'} className={cn( 'bg-surface-200 p-0 flex flex-row gap-0', - showTools ? '!min-w-[1000px]' : '!min-w-[600px]' + showTools ? '!min-w-[100vw] lg:!min-w-[1000px]' : '!min-w-[100vw] lg:!min-w-[600px]' )} >
diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx index 22c4bb7e252..70e2364f8b5 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx @@ -18,9 +18,9 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, - Tooltip_Shadcn_, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' import { generatePolicyCreateSQL } from './PolicyTableRow.utils' @@ -79,16 +79,16 @@ const PolicyRow = ({ role
{policy.roles.length > 3 && ( - - + + + {policy.roles.length - 3} more roles - - + + {policy.roles.slice(3).join(', ')} - - + + )}
diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx index 1397050ade5..3230f2a8a67 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx @@ -5,7 +5,7 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte import Panel from 'components/ui/Panel' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { Info } from 'lucide-react' -import { cn, Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui' +import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import PolicyRow from './PolicyRow' import PolicyTableRowHeader from './PolicyTableRowHeader' @@ -69,15 +69,15 @@ const PolicyTableRow = ({ Row Level Security is disabled. Your table is publicly readable and writable. - - + + - - + + Anyone with the project's anonymous key can modify or delete your data. Enable RLS and create access policies to keep your data secure. - - + + )} {policies.length === 0 && ( diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/TemplatePreview.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/TemplatePreview.tsx index a1dfa78a20c..4e41b304bb7 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/TemplatePreview.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/TemplatePreview.tsx @@ -9,7 +9,7 @@ interface TemplatePreviewProps { const TemplatePreview = ({ selectedTemplate }: TemplatePreviewProps) => { const { id, templateName, description, statement } = selectedTemplate return ( -
+
{!isEmpty(selectedTemplate) && (
diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/TemplatesList.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/TemplatesList.tsx index 96612b48403..9e7afbcb273 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/TemplatesList.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/TemplatesList.tsx @@ -16,9 +16,9 @@ const TemplatesList = ({ selectedTemplate, setSelectedTemplate = noop, }: TemplatesListProps) => ( -
+
diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/index.tsx index 44600f1caff..ec7b80c8b01 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTemplates/index.tsx @@ -20,7 +20,7 @@ const PolicyTemplates = ({ const [selectedTemplate, setSelectedTemplate] = useState(templates[0]) return (
-
+
+ Select Provider + + {subscription?.plan.id === 'free' ? ( diff --git a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/AddRLSPolicyForFirebaseDialog.tsx b/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/AddRLSPolicyForFirebaseDialog.tsx deleted file mode 100644 index 3f98c84a003..00000000000 --- a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/AddRLSPolicyForFirebaseDialog.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { copyToClipboard } from 'lib/helpers' -import { Check } from 'lucide-react' -import Link from 'next/link' -import { useEffect, useState } from 'react' -import { - Button, - DialogDescription, - DialogFooter, - DialogHeader, - DialogSection, - DialogSectionSeparator, - DialogTitle, -} from 'ui' -import { SQLCodeBlock } from './SqlCodeBlock' - -export const AddRLSPolicyForFirebaseDialog = ({ - projectRef, - firebaseProjectId, -}: { - projectRef: string - firebaseProjectId: string -}) => { - const description = `create policy - "Restrict access to Firebase Auth for project ID ${firebaseProjectId}" - on table_name - as restrictive - to authenticated using ( - ( - auth.jwt()->>'iss' = 'https://${projectRef}.supabase.co/auth/v1' - ) - or ( - auth.jwt()->>'iss' = 'https://securetoken.google.com/${firebaseProjectId}' - and auth.jwt()->>'aud' = '${firebaseProjectId}' - ) -);` - - const [copied, setCopied] = useState(false) - - useEffect(() => { - if (!copied) return - const timer = setTimeout(() => setCopied(false), 2000) - return () => clearTimeout(timer) - }, [copied]) - - function handleCopy(formatted: string) { - copyToClipboard(formatted, () => setCopied(true)) - } - - return ( - <> - - Add RLS policy - - We recommend adding a Row Level Security (RLS) policy for your Firebase JWT keys to - explicitly specify which tables can be accessed via the Data APIs in order to prevent - accidental exposure of data. - - - - - {[description]} - - - - - - - ) -} diff --git a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/CreateFirebaseAuthDialog.tsx b/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/CreateFirebaseAuthDialog.tsx index 32de8cae74d..4c49a1fefc1 100644 --- a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/CreateFirebaseAuthDialog.tsx +++ b/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/CreateFirebaseAuthDialog.tsx @@ -1,5 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { AlertTriangle, Trash } from 'lucide-react' +import { Trash } from 'lucide-react' import { useEffect } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' @@ -7,11 +7,7 @@ import * as z from 'zod' import { useParams } from 'common' import { useCreateThirdPartyAuthIntegrationMutation } from 'data/third-party-auth/integration-create-mutation' -import { useFlag } from 'hooks/ui/useFlag' import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, Button, Dialog, DialogContent, @@ -69,13 +65,6 @@ export const CreateFirebaseAuthIntegrationDialog = ({ }, }) - const allowedProjectIDs = useFlag('firebaseTPAAllowedProjectIDs') - - const isAvailable = - typeof allowedProjectIDs === 'string' - ? allowedProjectIDs.split(',').includes(projectRef!) - : !!allowedProjectIDs - useEffect(() => { if (visible) { form.reset({ @@ -109,11 +98,10 @@ export const CreateFirebaseAuthIntegrationDialog = ({ - {isAvailable ? ( - - - {/* Enabled flag can't be changed for now because there's no update API call for integrations */} - {/* + + {/* Enabled flag can't be changed for now because there's no update API call for integrations */} + {/* */} -

- This will enable a JWT token from a specific Firebase project to access data from - this Supabase project. -

- ( - - - - - - )} - /> - - - - - This connection requires a Row Level Security (RLS) policy - - - You will need to manually add a RLS policy after creating this Firebase - connection. Otherwise, the Supabase project will be accessible to anyone with a - valid Firebase JWT token. - - - -
- ) : ( -

- Third-party Auth with Firebase is currently in a private alpha release.{' '} - - Please submit your interest for it - {' '} - and someone from the Supabase team will reach out. You can also try it out via the - CLI. -

- )} +

+ This will enable a JWT token from a specific Firebase project to access data from + this Supabase project. +

+ ( + + + + + + )} + /> + +
- {isAvailable ? ( - <> - {!isCreating && ( -
- -
- )} - - - - - ) : ( - <> - - - +
)} + + + diff --git a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/IntegrationCard.tsx b/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/IntegrationCard.tsx index a8cb879cd4f..aab6710eec1 100644 --- a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/IntegrationCard.tsx +++ b/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/IntegrationCard.tsx @@ -1,11 +1,9 @@ -import { ExclamationCircleIcon } from '@heroicons/react/16/solid' import { Check } from 'lucide-react' import Image from 'next/image' import { useParams } from 'common' import { ThirdPartyAuthIntegration } from 'data/third-party-auth/integrations-query' -import { Badge, Button, Dialog, DialogContent, DialogTrigger } from 'ui' -import { AddRLSPolicyForFirebaseDialog } from './AddRLSPolicyForFirebaseDialog' +import { Badge, Button } from 'ui' import { AWS_IDP_REGIONS } from './AwsRegionSelector' import { getIntegrationType, @@ -177,38 +175,6 @@ export const IntegrationCard = ({ )}
- {type === 'firebase' ? ( -
-
- -
-
-
-

Firebase provider uses a custom RLS policy

-

- Be aware that this provider requires an RLS policy to correctly work. -

-
- - - - - - - - - -
-
- ) : null} ) } diff --git a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/SqlCodeBlock.tsx b/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/SqlCodeBlock.tsx deleted file mode 100644 index c0afb0d5e71..00000000000 --- a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/SqlCodeBlock.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { format } from 'sql-formatter' -import { CodeBlock, cn } from 'ui' - -interface SQLCodeBlockProps { - children: string[] - className?: string -} - -export const SQLCodeBlock = ({ children, className }: SQLCodeBlockProps) => { - let formatted = (children || [''])[0] - try { - formatted = format(formatted, { - language: 'postgresql', - keywordCase: 'upper', - }) - } catch {} - - if (formatted.length === 0) { - return null - } - - return ( -
-      code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap',
-          className
-        )}
-        hideCopy
-        hideLineNumbers
-      />
-    
- ) -} diff --git a/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx b/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx index 8f61d7b5055..d602f9fe963 100644 --- a/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx +++ b/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx @@ -9,9 +9,9 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, - Tooltip_Shadcn_, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' import CreateUserModal from './CreateUserModal' import InviteUserModal from './InviteUserModal' @@ -32,8 +32,8 @@ const AddUserDropdown = () => { - - + + {

Send invitation

-
+ {!canInviteUsers && ( - + You need additional permissions to invite users - + )} -
+ - - + + {

Create new user

-
+ {!canCreateUsers && ( - + You need additional permissions to create users - + )} -
+
diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx index 51dff52526f..ed1533b0bca 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx @@ -242,7 +242,7 @@ export const UsersV2 = () => { <>
-
+
{selectedUsers.size > 0 ? (
) : ( <> -
+
- + + {statusMapping?.label || status} - - + + {[InvoiceStatus.OPEN, InvoiceStatus.ISSUED, InvoiceStatus.UNCOLLECTIBLE].includes(status) && (paymentAttempted ? (

@@ -86,8 +86,8 @@ const InvoiceStatusBadge = ({ status, paymentAttempted }: InvoiceStatusBadgeProp This invoice has been forgiven. No action is required on your side.

)} -
- + + ) } diff --git a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx index cc4f888b760..7f184986165 100644 --- a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx @@ -111,7 +111,7 @@ const AddPaymentMethodForm = ({ } return ( -
+
@@ -147,16 +147,17 @@ const AddPaymentMethodForm = ({ - +
) } diff --git a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx index f57113f011b..55b6d5ad6c8 100644 --- a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx +++ b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx @@ -31,9 +31,9 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, - Tooltip_Shadcn_, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import BranchStatusBadge from './BranchStatusBadge' @@ -265,8 +265,8 @@ export const BranchRow = ({ - + {accessTokens.length === 0 && ( - - Create an access token to connect to your collection - + Create an access token to connect to your collection )} - + diff --git a/apps/studio/components/interfaces/DataWarehouse/WarehouseCollectionDetail.tsx b/apps/studio/components/interfaces/DataWarehouse/WarehouseCollectionDetail.tsx index 95c577f3304..6c26c0693fe 100644 --- a/apps/studio/components/interfaces/DataWarehouse/WarehouseCollectionDetail.tsx +++ b/apps/studio/components/interfaces/DataWarehouse/WarehouseCollectionDetail.tsx @@ -1,26 +1,24 @@ -import { RefreshCcw, Rewind, Terminal } from 'lucide-react' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' +import { useKeyboardShortcuts } from 'components/grid/components/common/Hooks' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import AlertError from 'components/ui/AlertError' import LoadingOpacity from 'components/ui/LoadingOpacity' import ShimmerLine from 'components/ui/ShimmerLine' import { useWarehouseAccessTokensQuery } from 'data/analytics/warehouse-access-tokens-query' +import { useWarehouseLogDetailQuery } from 'data/analytics/warehouse-collection-log-detail-query' import { useWarehouseCollectionsQuery } from 'data/analytics/warehouse-collections-query' import { useWarehouseQueryQuery } from 'data/analytics/warehouse-query' +import dayjs from 'dayjs' +import { useSelectedLog } from 'hooks/analytics/useSelectedLog' import { useFlag } from 'hooks/ui/useFlag' -import { Button } from 'ui' -import LogTable from '../Settings/Logs/LogTable' -import { TestCollectionDialog } from './TestCollectionDialog' -import { Tooltip, TooltipContent, TooltipTrigger } from '@ui/components/shadcn/ui/tooltip' -import { Input } from '@ui/components/shadcn/ui/input' +import { RefreshCcw, Rewind, Terminal } from 'lucide-react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import DatePickers from '../Settings/Logs/Logs.DatePickers' import { DatetimeHelper } from '../Settings/Logs/Logs.types' -import dayjs from 'dayjs' -import { useKeyboardShortcuts } from 'components/grid/components/common/Hooks' -import { useWarehouseLogDetailQuery } from 'data/analytics/warehouse-collection-log-detail-query' -import { useSelectedLog } from 'hooks/analytics/useSelectedLog' +import LogTable from '../Settings/Logs/LogTable' +import { TestCollectionDialog } from './TestCollectionDialog' const INTERVALS: DatetimeHelper[] = [ { diff --git a/apps/studio/components/interfaces/DataWarehouse/WarehouseMenuItem.tsx b/apps/studio/components/interfaces/DataWarehouse/WarehouseMenuItem.tsx index 78977d77feb..9b2c656e254 100644 --- a/apps/studio/components/interfaces/DataWarehouse/WarehouseMenuItem.tsx +++ b/apps/studio/components/interfaces/DataWarehouse/WarehouseMenuItem.tsx @@ -12,13 +12,13 @@ import { DropdownMenuItem, DropdownMenuSeparator, Modal, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, - Tooltip_Shadcn_, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' -import { LogsSidebarItem } from '../Settings/Logs/SidebarV2/SidebarItem' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { LogsSidebarItem } from '../Settings/Logs/SidebarV2/SidebarItem' import { CollectionForm } from './CollectionForm' type Props = { @@ -68,8 +68,8 @@ export const WarehouseMenuItem = ({ item }: Props) => { href={`/project/${projectRef}/logs/collections/${item.token}`} dropdownItems={ <> - - + + { @@ -80,16 +80,16 @@ export const WarehouseMenuItem = ({ item }: Props) => {
Update collection
-
+ {!canUpdateCollection && ( - + You need additional permissions to update collections - + )} -
+ - - + + { @@ -100,13 +100,13 @@ export const WarehouseMenuItem = ({ item }: Props) => {
Delete collection
-
+ {!canUpdateCollection && ( - + You need additional permissions to delete collections - + )} -
+ } > diff --git a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx index 5005de5146a..90deb1ac302 100644 --- a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx +++ b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx @@ -42,7 +42,7 @@ function DatabaseBackupsNav({ active }: Props) { ] as const return ( - + {navMenuItems.map( (item) => item.enabled && ( diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx index 41885f56381..7e6f70b3f3f 100644 --- a/apps/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx +++ b/apps/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx @@ -1,13 +1,12 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { Calendar } from 'lucide-react' import Link from 'next/link' import { useParams } from 'common' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FormPanel } from 'components/ui/Forms/FormPanel' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { Button } from 'ui' import { getPITRRetentionDuration } from './PITR.utils' const PITRNotice = ({}) => { @@ -28,32 +27,23 @@ const PITRNotice = ({}) => { You can also increase your recovery retention period updating your PITR add-on - - - - - {!canUpdateSubscription && ( - - - -
- - You need additional permissions to amend subscriptions - -
-
-
- )} -
+ + + Increase retention period + +
} > diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx index 3a84635d580..128a84e006e 100644 --- a/apps/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx +++ b/apps/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx @@ -1,4 +1,3 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import dayjs from 'dayjs' @@ -8,9 +7,9 @@ import { FormPanel } from 'components/ui/Forms/FormPanel' import { useBackupsQuery } from 'data/database/backups-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { Button } from 'ui' import type { Timezone } from './PITR.types' import { TimezoneSelection } from './TimezoneSelection' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' interface PITRStatusProps { selectedTimezone: Timezone @@ -59,38 +58,22 @@ const PITRStatus = ({ You'll be able to pick the right date and time when you begin
- - - - - {hasReadReplicas || - (!canTriggerPhysicalBackup && ( - - - -
- - {hasReadReplicas - ? 'You will need to remove all read replicas first to trigger a PITR recovery' - : !canTriggerPhysicalBackup - ? 'You need additional permissions to trigger a PITR recovery' - : null} - -
-
-
- ))} -
+ onSetConfiguration()} + tooltip={{ + content: { + side: 'left', + text: hasReadReplicas + ? 'You will need to remove all read replicas first to trigger a PITR recovery' + : !canTriggerPhysicalBackup + ? 'You need additional permissions to trigger a PITR recovery' + : undefined, + }, + }} + > + Start a restore +
} > diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/TimeInput.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/TimeInput.tsx index 4dc559481d6..e43aa3c2d91 100644 --- a/apps/studio/components/interfaces/Database/Backups/PITR/TimeInput.tsx +++ b/apps/studio/components/interfaces/Database/Backups/PITR/TimeInput.tsx @@ -1,9 +1,9 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import dayjs from 'dayjs' import { isNaN, noop } from 'lodash' import { Clock } from 'lucide-react' import { ChangeEvent, useEffect, useState } from 'react' +import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' import type { Time } from './PITR.types' import { formatNumberToTwoDigits, formatTimeToTimeString } from './PITR.utils' @@ -91,8 +91,8 @@ const TimeInput = ({ defaultTime, minimumTime, maximumTime, onChange = noop }: T ].join(' ')} > - - + + onInputChange(event, 'h')} className="w-full text-sm bg-transparent p-0 text-center outline-none border-none focus:ring-0" /> - - - - -
- Hours (HH) -
-
-
-
+ + + Hours (HH) + : - - + + onInputChange(event, 'm')} className="w-full text-sm bg-transparent p-0 text-center outline-none border-none focus:ring-0" /> - - - - -
- Minutes (MM) -
-
-
-
+ + + Minutes (MM) + : - - + + onInputChange(event, 's')} className="w-full text-sm bg-transparent p-0 text-center outline-none border-none focus:ring-0" /> - - - - -
- Seconds (SS) -
-
-
-
+ + Seconds (SS) +
{error &&

{error}

} diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx index 69207a2e0aa..d661beb01ee 100644 --- a/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx +++ b/apps/studio/components/interfaces/Database/Backups/PITR/pitr-form.tsx @@ -122,7 +122,7 @@ export function PITRForm({
} > -
+
{ - const { project } = useProjectContext() - - /** - * New project will have the same compute size and disk size as the original project - */ - function getAdditionalMonthlySpend() { - const currentProjectComputeSize = project?.infra_compute_size - if (!currentProjectComputeSize) { - return null - } - - if (currentProjectComputeSize === 'nano') { - return null - } - - const additionalMonthlySpend = instanceSizeSpecs[currentProjectComputeSize] - - return additionalMonthlySpend - } - - const additionalMonthlySpend = getAdditionalMonthlySpend() - if (!additionalMonthlySpend) { - return null - } +import { NewProjectPrice } from './RestoreToNewProject.utils' +export const AdditionalMonthlySpend = ({ + additionalMonthlySpend, +}: { + additionalMonthlySpend: NewProjectPrice +}) => { return (

@@ -36,9 +14,21 @@ export const AdditionalMonthlySpend = () => { Project Settings > Compute and Disk

-
-

Additional Monthly Compute + Disk Cost

-

${additionalMonthlySpend.priceMonthly}

+
+
+

Additional Monthly Compute

+

${additionalMonthlySpend.computePrice}

+
+
+

Additional Monthly Disk

+

${additionalMonthlySpend.diskPrice}

+
+
+

Total

+

+ ${additionalMonthlySpend.computePrice + additionalMonthlySpend.diskPrice} +

+
) diff --git a/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog.tsx b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog.tsx index 3aa8006be67..cc5b5c40dfe 100644 --- a/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog.tsx +++ b/apps/studio/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog.tsx @@ -11,17 +11,20 @@ import { DialogTitle, } from 'ui' import { AdditionalMonthlySpend } from './AdditionalMonthlySpend' +import { NewProjectPrice } from './RestoreToNewProject.utils' interface ConfirmRestoreDialogProps { open: boolean onOpenChange: (value: boolean) => void onSelectContinue: () => void + additionalMonthlySpend: NewProjectPrice } export const ConfirmRestoreDialog = ({ open, onOpenChange, onSelectContinue, + additionalMonthlySpend, }: ConfirmRestoreDialogProps) => { const { project } = useProjectContext() const organization = useSelectedOrganization() @@ -64,7 +67,7 @@ export const ConfirmRestoreDialog = ({ - + )} diff --git a/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx b/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx index 44386c6351f..ec454159e3f 100644 --- a/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx @@ -10,14 +10,7 @@ import { DatabaseExtension } from 'data/database-extensions/database-extensions- import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' import { extensions } from 'shared-data' -import { - Button, - cn, - Switch, - Tooltip_Shadcn_, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, -} from 'ui' +import { Button, cn, Switch, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { Admonition } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import EnableExtensionModal from './EnableExtensionModal' @@ -29,7 +22,6 @@ interface ExtensionCardProps { const ExtensionCard = ({ extension }: ExtensionCardProps) => { const { project } = useProjectContext() - const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || '' const isOn = extension.installed_version !== null const isOrioleDb = useIsOrioleDb() @@ -44,12 +36,10 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => { const disabled = !canUpdateExtensions || orioleDbCheck const X_PADDING = 'px-5' - const extensionMeta = extensions.find((item: any) => item.name === extension.name) + const extensionMeta = extensions.find((item) => item.name === extension.name) const docsUrl = extensionMeta?.link.startsWith('/guides') - ? siteUrl === 'http://localhost:8082' - ? `http://localhost:3001/docs${extensions.find((item) => item.name === extension.name)?.link}` - : `https://supabase.com/docs${extensions.find((item) => item.name === extension.name)?.link}` - : extensions.find((item: any) => item.name === extension.name)?.link ?? undefined + ? `https://supabase.com/docs${extensionMeta?.link}` + : extensionMeta?.link ?? undefined const { mutate: disableExtension, isLoading: isDisabling } = useDatabaseExtensionDisableMutation({ onSuccess: () => { @@ -87,8 +77,8 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => { {isDisabling ? ( ) : ( - - + + { isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true) } /> - + {disabled && ( - + {!canUpdateExtensions ? 'You need additional permissions to toggle extensions' : orioleDbCheck ? 'Project is using OrioleDB and cannot be disabled' : null} - + )} - + )}
diff --git a/apps/studio/components/interfaces/Database/Functions/CreateFunction/CreateFunctionHeader.tsx b/apps/studio/components/interfaces/Database/Functions/CreateFunction/CreateFunctionHeader.tsx index c3354a36930..de1260fac5d 100644 --- a/apps/studio/components/interfaces/Database/Functions/CreateFunction/CreateFunctionHeader.tsx +++ b/apps/studio/components/interfaces/Database/Functions/CreateFunction/CreateFunctionHeader.tsx @@ -31,8 +31,8 @@ export const CreateFunctionHeader = ({ : 'Add a new function'}
- {/* - + {/* + - - + + {assistantVisible ? 'Hide' : 'Show'} tools - - */} + + */} ) } diff --git a/apps/studio/components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx b/apps/studio/components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx index 37db03c783e..3eb7081c078 100644 --- a/apps/studio/components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx +++ b/apps/studio/components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx @@ -1,15 +1,7 @@ import { Maximize2, Minimize2 } from 'lucide-react' -import SqlEditor from 'components/ui/SqlEditor' -import { - Button, - FormControl_Shadcn_, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, - Tooltip_Shadcn_, - cn, -} from 'ui' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' +import { Button, FormControl_Shadcn_, Tooltip, TooltipContent, TooltipTrigger, cn } from 'ui' export const FunctionEditor = ({ field, @@ -41,8 +33,8 @@ export const FunctionEditor = ({ 'opacity-0 group-hover:opacity-100 group-hover:top-2 transition-all' )} > - - + +
) diff --git a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx index 54f5317c97c..bcbbb48bfe2 100644 --- a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx +++ b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx @@ -156,7 +156,7 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => { className={cn( // 'bg-surface-200', 'p-0 flex flex-row gap-0', - assistantVisible ? '!min-w-[1200px]' : '!min-w-[600px]' + assistantVisible ? '!min-w-screen lg:!min-w-[1200px]' : '!min-w-screen lg:!min-w-[600px]' )} >
diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx index 3f6010f3dfd..f06a0d3bade 100644 --- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx +++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx @@ -101,10 +101,10 @@ const FunctionsList = ({
) : (
-
-
+
+
} value={filterString} - className="w-52" + className="w-full lg:w-52" onChange={(e) => setFilterString(e.target.value)} />
@@ -131,6 +131,7 @@ const FunctionsList = ({ createFunction()} + className="flex-grow" tooltip={{ content: { side: 'bottom', diff --git a/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx b/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx index 5d4b4b03336..74252769dc9 100644 --- a/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx +++ b/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx @@ -109,7 +109,7 @@ const Indexes = () => { )} {isSuccessSchemas && ( { setSearch(e.target.value)} placeholder="Search for an index" icon={} @@ -127,7 +127,7 @@ const Indexes = () => { {!isLocked && (
Schema, - Table, - Name, - , - ]} - body={ - <> - {sortedIndexes.length === 0 && search.length === 0 && ( - - -

No indexes created yet

-

- There are no indexes found in the schema "{selectedSchema}" -

-
-
- )} - {sortedIndexes.length === 0 && search.length > 0 && ( - - -

No results found

-

- Your search for "{search}" did not return any results -

-
-
- )} - {indexes.length > 0 && - indexes.map((index) => ( - - -

{index.schema}

-
- -

{index.table}

-
- -

{index.name}

-
- -
- - {!isLocked && ( -
+
+
Schema, + Table, + Name, + , + ]} + body={ + <> + {sortedIndexes.length === 0 && search.length === 0 && ( + + +

No indexes created yet

+

+ There are no indexes found in the schema "{selectedSchema}" +

- ))} - - } - /> + )} + {sortedIndexes.length === 0 && search.length > 0 && ( + + +

No results found

+

+ Your search for "{search}" did not return any results +

+
+
+ )} + {indexes.length > 0 && + indexes.map((index) => ( + + +

{index.schema}

+
+ +

{index.table}

+
+ +

{index.name}

+
+ +
+ + {!isLocked && ( +
+
+
+ ))} + + } + /> + )} diff --git a/apps/studio/components/interfaces/Database/Migrations/MigrationsEmptyState.tsx b/apps/studio/components/interfaces/Database/Migrations/MigrationsEmptyState.tsx index 13283812b59..f827dd88ebe 100644 --- a/apps/studio/components/interfaces/Database/Migrations/MigrationsEmptyState.tsx +++ b/apps/studio/components/interfaces/Database/Migrations/MigrationsEmptyState.tsx @@ -46,8 +46,8 @@ const MigrationsEmptyState = () => { ] return ( -
-
+
+

Database migrations

diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx index ff3fe002a89..a3be0cde9bb 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsList.tsx @@ -99,60 +99,62 @@ const PublicationsList = ({ onSelectPublication = noop }: PublicationsListProps)

-
Name, - System ID, - Insert, - Update, - Delete, - Truncate, - - Source - , - ]} - body={ - isLoading - ? Array.from({ length: 5 }).map((_, i) => ) - : publications.map((x) => ( - - {x.name} - {x.id} - {publicationEvents.map((event) => ( - - { - setToggleListenEventValue({ - publication: x, - event, - currentStatus: (x as any)[event.key], - }) - }} - /> +
+
Name, + System ID, + Insert, + Update, + Delete, + Truncate, + + Source + , + ]} + body={ + isLoading + ? Array.from({ length: 5 }).map((_, i) => ) + : publications.map((x) => ( + + {x.name} + {x.id} + {publicationEvents.map((event) => ( + + { + setToggleListenEventValue({ + publication: x, + event, + currentStatus: (x as any)[event.key], + }) + }} + /> + + ))} + +
+ +
- ))} - -
- -
-
-
- )) - } - /> + + )) + } + /> + {!isLoading && publications.length === 0 && ( { -
+
{(Object.keys(ROLE_PERMISSIONS) as (keyof typeof ROLE_PERMISSIONS)[]).map( (permission) => ( @@ -175,18 +175,18 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { afterLabel={ !disabled && ROLE_PERMISSIONS[permission].disabled && ( - - + + - - + + This privilege cannot be updated via the dashboard - - + + ) } /> diff --git a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx index c529d9f348a..3726a78c7d4 100644 --- a/apps/studio/components/interfaces/Database/Roles/RolesList.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RolesList.tsx @@ -1,22 +1,21 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { partition, sortBy } from 'lodash' import { Plus, Search, X } from 'lucide-react' import { useState } from 'react' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import NoSearchResults from 'components/ui/NoSearchResults' import SparkBar from 'components/ui/SparkBar' import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' import { useMaxConnectionsQuery } from 'data/database/max-connections-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { Badge, Button, Input } from 'ui' +import { Badge, Button, Input, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import CreateRolePanel from './CreateRolePanel' import DeleteRoleModal from './DeleteRoleModal' import RoleRow from './RoleRow' import RoleRowSkeleton from './RoleRowSkeleton' import { SUPABASE_ROLES } from './Roles.constants' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' type SUPABASE_ROLE = (typeof SUPABASE_ROLES)[number] @@ -109,8 +108,8 @@ const RolesList = () => {
- - + +
{ labelBottomClass="text-xs" />
-
- - -
-

Connections by roles:

- {rolesWithActiveConnections.map((role) => ( -
- {role.name}: {role.activeConnections} -
- ))} -
-
-
+ + +

Connections by roles:

+ {rolesWithActiveConnections.map((role) => ( +
+ {role.name}: {role.activeConnections} +
+ ))} +
+ {
-
+

Roles managed by Supabase

Protected
@@ -194,7 +185,7 @@ const RolesList = () => {
-
+

Other database roles

diff --git a/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx b/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx index f3f9597eabe..f60cc72a3d9 100644 --- a/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx +++ b/apps/studio/components/interfaces/Database/Schemas/SchemaTableNode.tsx @@ -1,8 +1,8 @@ -import { DiamondIcon, ExternalLink, Fingerprint, Hash, Key, Link2, Table2 } from 'lucide-react' -import { Handle, NodeProps } from 'reactflow' +import { DiamondIcon, ExternalLink, Fingerprint, Hash, Key, Table2 } from 'lucide-react' import Link from 'next/link' +import { Handle, NodeProps } from 'reactflow' -import { Button, cn, Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui' +import { Button, cn } from 'ui' // ReactFlow is scaling everything by the factor of 2 const TABLE_NODE_WIDTH = 320 @@ -24,7 +24,12 @@ export type TableNodeData = { }[] } -const TableNode = ({ data, targetPosition, sourcePosition }: NodeProps) => { +const TableNode = ({ + data, + targetPosition, + sourcePosition, + placeholder, +}: NodeProps & { placeholder?: boolean }) => { // Important styles is a nasty hack to use Handles (required for edges calculations), but do not show them in the UI. // ref: https://github.com/wbkd/react-flow/discussions/2698 const hiddenNodeConnector = '!h-px !w-px !min-w-0 !min-h-0 !cursor-grab !border-0 !opacity-0' @@ -60,7 +65,7 @@ const TableNode = ({ data, targetPosition, sourcePosition }: NodeProps {data.name}
- {data.id && ( + {data.id && !placeholder && (
- -
- ))} + ))} +
-
- - + + + +
+ setFilterString(e.target.value)} + icon={} + /> - setFilterString(e.target.value)} - icon={} - /> - - {!isLocked && ( - } - disabled={!canUpdateTables} - onClick={() => onAddTable()} - tooltip={{ - content: { - side: 'bottom', - text: !canUpdateTables - ? 'You need additional permissions to create tables' - : undefined, - }, - }} - > - New table - - )} + {!isLocked && ( + } + disabled={!canUpdateTables} + onClick={() => onAddTable()} + tooltip={{ + content: { + side: 'bottom', + text: !canUpdateTables + ? 'You need additional permissions to create tables' + : undefined, + }, + }} + > + New table + + )} +
{isLocked && } @@ -364,8 +366,8 @@ const TableList = ({ entities.map((x) => ( - - + + {x.type === ENTITY_TYPE.TABLE ? ( )} - - + + {formatTooltipText(x.type)} - - + + {/* only show tooltips if required, to reduce noise */} {x.name.length > 20 ? ( - - +

{x.name}

-
- - - -
- {x.name} -
-
-
-
+ + + {x.name} + ) : (

{x.name}

)} @@ -493,8 +484,8 @@ const TableList = ({ {x.type === ENTITY_TYPE.TABLE && ( <> - - + +

Edit table

-
+ {!canUpdateTables && ( - + You need additional permissions to edit this table - + )} -
+ Duplicate Table - - + +

Delete table

-
+ {!canUpdateTables && ( - + You need additional permissions to delete tables - + )} -
+ )} diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx index 58c8282bcfd..ee072d51856 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx @@ -15,9 +15,9 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - Tooltip_Shadcn_, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' import { generateTriggerCreateSQL } from './TriggerList.utils' @@ -84,14 +84,14 @@ const TriggerList = ({ {_triggers.map((x: any) => ( - - + + {x.name} - - + + {x.name} - - + + diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx index 8f02b21f250..ee6c1608fed 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx @@ -89,10 +89,10 @@ const TriggersList = ({ ) : (
-
-
+
+
} value={filterString} - className="w-52" + className="w-full lg:w-52" onChange={(e) => setFilterString(e.target.value)} />
@@ -112,6 +112,7 @@ const TriggersList = ({ createTrigger()} + className="flex-grow" tooltip={{ content: { side: 'bottom', @@ -158,30 +159,32 @@ const TriggersList = ({ {isLocked && } -
- Name - Table - Function - Events - Orientation - - Enabled - - - - } - body={ - - } - /> +
+
+ Name + Table + Function + Events + Orientation + + Enabled + + + + } + body={ + + } + /> + )} diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts index 217242a53ee..027d44fb0d7 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts @@ -1,4 +1,5 @@ import { z } from 'zod' +import { ComputeInstanceAddonVariantId } from './DiskManagement.types' import { calculateDiskSizeRequiredForIopsWithGp3, calculateDiskSizeRequiredForIopsWithIo2, @@ -7,7 +8,6 @@ import { calculateMaxIopsAllowedForDiskSizeWithio2, formatNumber, } from './DiskManagement.utils' -import { ComputeInstanceAddonVariantId } from './DiskManagement.types' import { DISK_LIMITS, DiskType, IOPS_RANGE, THROUGHPUT_RANGE } from './ui/DiskManagement.constants' const baseSchema = z.object({ @@ -21,11 +21,31 @@ const baseSchema = z.object({ computeSize: z .custom((val): val is ComputeInstanceAddonVariantId => true) .describe('Compute size'), + growthPercent: z + .number() + .int('Value must be an integer') + .min(10, 'Growth percent must be at least 10%') + .max(100, 'Growth percent cannot exceed 100%') + .optional() + .nullable(), + minIncrementGb: z + .number() + .int('Value must be an integer') + .min(1, 'Minimum increment must be at least 1 GB') + .max(200, 'Minimum increment cannot exceed 200 GB') + .optional() + .nullable(), + maxSizeGb: z + .number() + .int('Value must be an integer') + .max(60000, 'Maximum size cannot exceed 60TB') + .optional() + .nullable(), }) export const CreateDiskStorageSchema = (defaultTotalSize: number) => { const schema = baseSchema.superRefine((data, ctx) => { - const { storageType, totalSize, provisionedIOPS, throughput, computeSize } = data + const { storageType, totalSize, provisionedIOPS, throughput, maxSizeGb } = data if (totalSize < defaultTotalSize) { ctx.addIssue({ @@ -35,6 +55,15 @@ export const CreateDiskStorageSchema = (defaultTotalSize: number) => { }) } + // Validate maxSizeGb cannot be lower than totalSize + if (!!maxSizeGb && maxSizeGb < totalSize) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Max disk size cannot be lower than the current disk size. Must be at least ${formatNumber(totalSize)} GB.`, + path: ['maxSizeGb'], + }) + } + if (storageType === 'io2') { // Validation rules for io2 diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 2080541b90b..84a224ed922 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -18,6 +18,8 @@ import { useRemainingDurationForDiskAttributeUpdate, } from 'data/config/disk-attributes-query' import { useUpdateDiskAttributesMutation } from 'data/config/disk-attributes-update-mutation' +import { useDiskAutoscaleCustomConfigQuery } from 'data/config/disk-autoscale-config-query' +import { useUpdateDiskAutoscaleConfigMutation } from 'data/config/disk-autoscale-config-update-mutation' import { useDiskUtilizationQuery } from 'data/config/disk-utilization-query' import { setProjectStatus } from 'data/projects/projects-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' @@ -46,6 +48,7 @@ import { DiskManagementMessage } from './DiskManagement.types' import { mapComputeSizeNameToAddonVariantId } from './DiskManagement.utils' import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection' import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog' +import { AutoScaleFields } from './fields/AutoScaleFields' import { ComputeSizeField } from './fields/ComputeSizeField' import { DiskSizeField } from './fields/DiskSizeField' import { IOPSField } from './fields/IOPSField' @@ -61,11 +64,8 @@ import { NoticeBar } from './ui/NoticeBar' import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection' export function DiskManagementForm() { - const { - project, - // isLoading is used to avoid a useCheckPermissions() race condition - isLoading: isProjectLoading, - } = useProjectContext() + // isLoading is used to avoid a useCheckPermissions() race condition + const { project, isLoading: isProjectLoading } = useProjectContext() const org = useSelectedOrganization() const { ref: projectRef } = useParams() const queryClient = useQueryClient() @@ -136,12 +136,15 @@ export function DiskManagementForm() { const { data: subscription, isSuccess: isSubscriptionSuccess } = useOrgSubscriptionQuery({ orgSlug: org?.slug, }) + const { data: diskAutoscaleConfig, isSuccess: isDiskAutoscaleConfigSuccess } = + useDiskAutoscaleCustomConfigQuery({ projectRef }) /** * Handle default values */ // @ts-ignore const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0 } + const { growth_percent, max_size_gb, min_increment_gb } = diskAutoscaleConfig ?? {} const defaultValues = { storageType: type ?? DiskType.GP3, provisionedIOPS: iops, @@ -150,6 +153,9 @@ export function DiskManagementForm() { computeSize: project?.infra_compute_size ? mapComputeSizeNameToAddonVariantId(project?.infra_compute_size) : undefined, + growthPercent: growth_percent, + minIncrementGb: min_increment_gb, + maxSizeGb: max_size_gb, } const form = useForm({ @@ -169,6 +175,7 @@ export function DiskManagementForm() { isDiskUtilizationSuccess && isReadReplicasSuccess && isSubscriptionSuccess && + isDiskAutoscaleConfigSuccess && isCooldownSuccess const isRequestingChanges = data?.requested_modification !== undefined @@ -181,6 +188,9 @@ export function DiskManagementForm() { const usedPercentage = (usedSize / totalSize) * 100 const isFlyArchitecture = project?.cloud_provider === 'FLY' + const disableIopsThroughputConfig = + RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(form.watch('computeSize')) && + subscription?.plan.id !== 'free' const disableDiskInputs = isRequestingChanges || @@ -205,19 +215,23 @@ export function DiskManagementForm() { // this is to suppress to toast message onError: () => {}, onSuccess: () => { - /** - * Manually set project status to RESIZING - * Project status should be RESIZING on next project status request. - */ + //Manually set project status to RESIZING, Project status should be RESIZING on next project status request. setProjectStatus(queryClient, projectRef!, PROJECT_STATUS.RESIZING) }, }) + const { mutateAsync: updateDiskAutoscaleConfig, isLoading: isUpdatingDiskAutoscaleConfig } = + useUpdateDiskAutoscaleConfigMutation({ + // this is to suppress to toast message + onError: () => {}, + }) - const isUpdatingConfig = isUpdatingDisk || isUpdatingCompute + const isUpdatingConfig = isUpdatingDisk || isUpdatingCompute || isUpdatingDiskAutoscaleConfig const onSubmit = async (data: DiskStorageSchemaType) => { let payload = data + let willUpdateDiskConfiguration = false setMessageState(null) + try { if ( payload.storageType !== form.formState.defaultValues?.storageType || @@ -225,6 +239,8 @@ export function DiskManagementForm() { payload.throughput !== form.formState.defaultValues?.throughput || payload.totalSize !== form.formState.defaultValues?.totalSize ) { + willUpdateDiskConfiguration = true + if (RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(payload.computeSize)) { payload.provisionedIOPS = IOPS_RANGE[DiskType.GP3].min } @@ -238,6 +254,19 @@ export function DiskManagementForm() { }) } + if ( + payload.growthPercent !== form.formState.defaultValues?.growthPercent || + payload.minIncrementGb !== form.formState.defaultValues?.minIncrementGb || + payload.maxSizeGb !== form.formState.defaultValues?.maxSizeGb + ) { + await updateDiskAutoscaleConfig({ + projectRef, + growthPercent: payload.growthPercent, + minIncrementGb: payload.minIncrementGb, + maxSizeGb: payload.maxSizeGb, + }) + } + if (payload.computeSize !== form.formState.defaultValues?.computeSize) { await updateSubscriptionAddon({ projectRef: projectRef, @@ -250,6 +279,9 @@ export function DiskManagementForm() { setIsDialogOpen(false) form.reset(data as DiskStorageSchemaType) + toast.success( + `Successfully updated disk settings!${willUpdateDiskConfiguration ? ' The requested changes will be applied to your disk shortly.' : ''}` + ) } catch (error: unknown) { setMessageState({ message: error instanceof Error ? error.message : 'An unknown error occurred', @@ -365,9 +397,9 @@ export function DiskManagementForm() {
Advanced disk settings - - Specify additional settings for your disk, including IOPS, throughput, and - disk type. + + Specify additional settings for your disk, including autoscaling + configuration, IOPS, throughput, and disk type.
- - { - form.setValue('computeSize', 'ci_large') - }} - > - Change to LARGE Compute - - } - /> - - +
+ +
+ +
+ + { + form.setValue('computeSize', 'ci_large') + }} + > + Change to LARGE Compute + + } + /> + + +
diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx index b898d8e009c..7f42ba51bce 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx @@ -41,9 +41,9 @@ import { RadioGroupCard, RadioGroupCardItem, Separator, - Tooltip_Shadcn_, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' import { Admonition } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -74,6 +74,7 @@ import { import { NoticeBar } from './ui/NoticeBar' import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection' +// [Joshen] Only used for non AWS projects export function DiskManagementPanelForm() { const { project } = useProjectContext() const org = useSelectedOrganization() @@ -436,20 +437,20 @@ export function DiskManagementPanelForm() { ? `between ${minIOPS} and ${maxIOPS.toLocaleString()} based on your disk size.` : `at least ${minIOPS}`} - - + + - - + + For io2 storage type, min IOPS is at {minIOPS}, while max IOPS is at 1000 * disk size in GB or{' '} {IOPS_RANGE[DiskType.IO2].max.toLocaleString()}, whichever is lower - - + + ) : (
@@ -459,20 +460,20 @@ export function DiskManagementPanelForm() { ? `between ${minIOPS.toLocaleString()} and ${maxIOPS.toLocaleString()} based on your disk size.` : `at least ${minIOPS.toLocaleString()}`} - - + + - - + + For gp3 storage type, min IOPS is at {minIOPS} while max IOPS is at 500 * disk size in GB or{' '} {IOPS_RANGE[DiskType.GP3].max.toLocaleString()}, whichever is lower - - + +
)} {!form.formState.errors.provisionedIOPS && @@ -558,18 +559,18 @@ export function DiskManagementPanelForm() { Throughput must be between {minThroughput.toLocaleString()}{' '} and {maxThroughput?.toLocaleString()} MB/s based on your IOPS. - - + + - - + + Min throughput is at 125MB/s, while max throughput is at 0.25MB/s * IOPS or 1,000, whichever is lower - - + + {!form.formState.errors.throughput && field.value !== undefined && diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx index 536403d8694..f909aba99b4 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog.tsx @@ -45,7 +45,7 @@ import { } from './DiskManagement.utils' import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection' import { BillingChangeBadge } from './ui/BillingChangeBadge' -import { DiskType } from './ui/DiskManagement.constants' +import { DISK_AUTOSCALE_CONFIG_DEFAULTS, DiskType } from './ui/DiskManagement.constants' import { DiskMangementCoolDownSection } from './ui/DiskManagementCoolDownSection' const TableHeaderRow = () => ( @@ -209,6 +209,9 @@ export const DiskManagementReviewAndSubmitDialog = ({ numReplicas, }) + const hasComputeChanges = + form.formState.defaultValues?.computeSize !== form.getValues('computeSize') + const hasDiskConfigChanges = form.formState.defaultValues?.provisionedIOPS !== form.getValues('provisionedIOPS') || (form.formState.defaultValues?.throughput !== form.getValues('throughput') && @@ -248,15 +251,19 @@ export const DiskManagementReviewAndSubmitDialog = ({ Changes will be applied shortly once confirmed. -
- - -
- + {(hasComputeChanges || hasDiskConfigChanges) && ( + <> +
+ + +
+ + + )}
@@ -325,11 +332,56 @@ export const DiskManagementReviewAndSubmitDialog = ({ priceTooltip={numReplicas > 0 ? replicaTooltipText : undefined} /> )} + {form.formState.defaultValues?.growthPercent !== form.getValues('growthPercent') && ( + + )} + {form.formState.defaultValues?.minIncrementGb !== form.getValues('minIncrementGb') && ( + + )} + {form.formState.defaultValues?.maxSizeGb !== form.getValues('maxSizeGb') && ( + + )}
- {/* */} - diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/GetStartedHero.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/GetStartedHero.tsx index c9469de051f..c4f503a1932 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/GetStartedHero.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/GetStartedHero.tsx @@ -1,52 +1,85 @@ +import { cn } from 'ui' + +const Checkbox = () => ( +
+
+
+) + +const Row = ({ + className, + col1, + col2, + col3, +}: { + className?: string + col1: string + col2: string + col3: string +}) => ( +
+ +
+

{col1}

+
+
+

{col2}

+
+
+

{col3}

+
+
+) + const GetStartedHero = () => { return ( -
+
1

- create table countries {`(`} + create table todos {`(`}

2

- id bigint generated by default + id bigint generated by default,

3

- name text + task text,

4

- iso2 text not null + status status default 'Not Started',

5

- iso3 text + user_id uuid references auth.users not null,

6

- local_name text + inserted_at timestamp with time zone,

7

- continent continents + updated_at timestamp with time zone,

@@ -55,123 +88,19 @@ const GetStartedHero = () => {
-
-
-
-
-
-

id

-
-
-

name

-
-
-

iso2

-
-
-
-
-
-
-
-

1

-
-
-

Angola

-
-
-

AO

-
-
-
-
-
-
-
-

2

-
-
-

Timor-Leste

-
-
-

TL

-
-
-
-
-
-
-
-

3

-
-
-

Serbia

-
-
-

RS

-
-
-
-
-
-
-
-

4

-
-
-

Bahamas

-
-
-

BS

-
-
-
-
-
-
-
-

5

-
-
-

Chile

-
-
-

CL

-
-
-
-
-
-
-
-

6

-
-
-

Denmark

-
-
-

DK

-
-
-
-
-
-
-
-

7

-
-
-

Singapore

-
-
-

SG

-
-
+ + + + + + + +
) diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx index 2a7d974d274..96bf3c8dc3b 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx @@ -22,7 +22,7 @@ const NewProjectPanel = () => { return (
-
+

Welcome to your new project

diff --git a/apps/studio/components/interfaces/Home/ProjectList/ProjectCardStatus.tsx b/apps/studio/components/interfaces/Home/ProjectList/ProjectCardStatus.tsx index bfa4300c2a0..e797bd3eaf0 100644 --- a/apps/studio/components/interfaces/Home/ProjectList/ProjectCardStatus.tsx +++ b/apps/studio/components/interfaces/Home/ProjectList/ProjectCardStatus.tsx @@ -3,14 +3,7 @@ import { AlertTriangle, Info, PauseCircle, RefreshCcw } from 'lucide-react' import { RESOURCE_WARNING_MESSAGES } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants' import { getWarningContent } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils' import type { ResourceWarning } from 'data/usage/resource-warnings-query' -import { - Alert_Shadcn_, - AlertTitle_Shadcn_, - cn, - Tooltip_Shadcn_, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, -} from 'ui' +import { Alert_Shadcn_, AlertTitle_Shadcn_, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { InferredProjectStatus } from './ProjectCard.utils' export interface ProjectCardWarningsProps { @@ -137,12 +130,12 @@ export const ProjectCardStatus = ({ )}
{alertTitle} - - + + - - {alertDescription} - + + {alertDescription} +
) diff --git a/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx b/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx index 46002fece18..d3411fa11d6 100644 --- a/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx +++ b/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx @@ -86,7 +86,7 @@ const ProjectList = ({ return (
@@ -263,7 +263,7 @@ const OrganizationProjects = ({ ) : ( -
    +
      {isErrorPermissions ? (
      { 'project_storage:all', ]) - const [interval, setInterval] = useState('hourly') + const [interval, setInterval] = useState('hourly') const { data, isLoading } = useProjectLogStatsQuery({ projectRef, interval }) @@ -92,7 +96,12 @@ const ProjectUsage = () => { - + + setInterval(interval as ProjectLogStatsVariables['interval']) + } + > {CHART_INTERVALS.map((i) => ( {i.label} diff --git a/apps/studio/components/interfaces/Home/ServiceStatus.tsx b/apps/studio/components/interfaces/Home/ServiceStatus.tsx index 146c58ef5a0..2c189366dfd 100644 --- a/apps/studio/components/interfaces/Home/ServiceStatus.tsx +++ b/apps/studio/components/interfaces/Home/ServiceStatus.tsx @@ -1,12 +1,15 @@ import dayjs from 'dayjs' -import { AlertTriangle, CheckCircle2, ChevronRight, Info, Loader2 } from 'lucide-react' +import { AlertTriangle, CheckCircle2, ChevronRight, Loader2 } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { useParams } from 'common' import { useEdgeFunctionServiceStatusQuery } from 'data/service-status/edge-functions-status-query' import { usePostgresServiceStatusQuery } from 'data/service-status/postgres-service-status-query' -import { useProjectServiceStatusQuery } from 'data/service-status/service-status-query' +import { + ProjectServiceStatus, + useProjectServiceStatusQuery, +} from 'data/service-status/service-status-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { @@ -21,6 +24,7 @@ import { PopoverSeparator } from '@ui/components/shadcn/ui/popover' const SERVICE_STATUS_THRESHOLD = 5 // minutes const StatusMessage = ({ + status, isLoading, isSuccess, isProjectNew, @@ -28,25 +32,41 @@ const StatusMessage = ({ isLoading: boolean isSuccess: boolean isProjectNew: boolean + status?: ProjectServiceStatus }) => { if (isLoading) return 'Checking status' if (isProjectNew) return 'Coming up...' if (isSuccess) return 'No issues' + if (status === 'UNHEALTHY') return 'Unhealthy' + if (status === 'COMING_UP') return 'Coming up...' + if (status === 'ACTIVE_HEALTHY') return 'Healthy' return 'Unable to connect' } +const iconProps = { + size: 18, + strokeWidth: 1.5, +} +const LoaderIcon = () => +const AlertIcon = () => +const CheckIcon = () => + const StatusIcon = ({ isLoading, isSuccess, isProjectNew, + projectStatus, }: { isLoading: boolean isSuccess: boolean isProjectNew: boolean + projectStatus?: ProjectServiceStatus }) => { - if (isLoading || isProjectNew) return - if (isSuccess) return - return + if (isLoading || isProjectNew) return + if (isSuccess) return + if (projectStatus === 'UNHEALTHY') return + if (projectStatus === 'COMING_UP') return + return } const ServiceStatus = () => { @@ -73,17 +93,36 @@ const ServiceStatus = () => { data: status, isLoading, refetch: refetchServiceStatus, - } = useProjectServiceStatusQuery({ projectRef: ref }) + } = useProjectServiceStatusQuery( + { + projectRef: ref, + }, + { + refetchInterval: (data) => (data?.some((service) => !service.healthy) ? 5000 : false), + } + ) const { data: edgeFunctionsStatus, refetch: refetchEdgeFunctionServiceStatus } = - useEdgeFunctionServiceStatusQuery({ projectRef: ref }) + useEdgeFunctionServiceStatusQuery( + { + projectRef: ref, + }, + { + refetchInterval: (data) => (!data?.healthy ? 5000 : false), + } + ) const { isLoading: isLoadingPostgres, isSuccess: isSuccessPostgres, refetch: refetchPostgresServiceStatus, - } = usePostgresServiceStatusQuery({ - projectRef: ref, - connectionString: project?.connectionString, - }) + } = usePostgresServiceStatusQuery( + { + projectRef: ref, + connectionString: project?.connectionString, + }, + { + refetchInterval: (data) => (data === null ? 5000 : false), + } + ) const authStatus = status?.find((service) => service.name === 'auth') const restStatus = status?.find((service) => service.name === 'rest') @@ -98,6 +137,7 @@ const ServiceStatus = () => { isLoading: boolean isSuccess?: boolean logsUrl: string + status?: ProjectServiceStatus }[] = [ { name: 'Database', @@ -113,6 +153,7 @@ const ServiceStatus = () => { docsUrl: undefined, isLoading, isSuccess: restStatus?.healthy, + status: restStatus?.status, logsUrl: '/logs/postgrest-logs', }, ...(authEnabled @@ -123,6 +164,7 @@ const ServiceStatus = () => { docsUrl: undefined, isLoading, isSuccess: authStatus?.healthy, + status: authStatus?.status, logsUrl: '/logs/auth-logs', }, ] @@ -135,6 +177,7 @@ const ServiceStatus = () => { docsUrl: undefined, isLoading, isSuccess: realtimeStatus?.healthy, + status: realtimeStatus?.status, logsUrl: '/logs/realtime-logs', }, ] @@ -147,6 +190,7 @@ const ServiceStatus = () => { docsUrl: undefined, isLoading, isSuccess: storageStatus?.healthy, + status: storageStatus?.status, logsUrl: '/logs/storage-logs', }, ] @@ -202,7 +246,7 @@ const ServiceStatus = () => { type="default" icon={ isLoadingChecks || isProjectNew ? ( - + ) : (
      { isLoading={service.isLoading} isSuccess={!!service.isSuccess} isProjectNew={isProjectNew} + projectStatus={service.status} />

      {service.name}

      @@ -235,6 +280,7 @@ const ServiceStatus = () => { isLoading={service.isLoading} isSuccess={!!service.isSuccess} isProjectNew={isProjectNew} + status={service.status} />

      @@ -245,13 +291,17 @@ const ServiceStatus = () => {
      ))} - -
      -
      - -
      - Recently restored projects can take up to 5 minutes to become fully operational. -
      + {allServicesOperational ? null : ( + <> + +
      +
      + +
      + Recently restored projects can take up to 5 minutes to become fully operational. +
      + + )} ) diff --git a/apps/studio/components/interfaces/HomePageActions.tsx b/apps/studio/components/interfaces/HomePageActions.tsx index 2bda86c5e22..05a35690955 100644 --- a/apps/studio/components/interfaces/HomePageActions.tsx +++ b/apps/studio/components/interfaces/HomePageActions.tsx @@ -21,6 +21,7 @@ import { PopoverTrigger_Shadcn_, Popover_Shadcn_, } from 'ui' +import { useFlag } from 'hooks/ui/useFlag' interface HomePageActionsProps { organizations: { name: string; slug: string }[] @@ -42,6 +43,9 @@ const HomePageActions = ({ const organizationCreationEnabled = useIsFeatureEnabled('organizations:create') const { isSuccess: orgsLoaded } = useOrganizationsQuery() + const projectCreationExperimentGroup = useFlag('projectCreationExperimentGroup') + const newProjectPath = projectCreationExperimentGroup === 'group-b' ? '/new/v2' : '/new' + return (
      @@ -56,7 +60,10 @@ const HomePageActions = ({ {organizations .sort((a, b) => a.name.localeCompare(b.name)) .map((x) => ( - router.push(`/new/${x.slug}`)}> + router.push(`${newProjectPath}/${x.slug}`)} + > {x.name} ))} diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx index 51499cce162..2e25afb4ee2 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx @@ -1,11 +1,13 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { toString as CronToString } from 'cronstrue' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import z from 'zod' +import { useWatch } from '@ui/components/shadcn/ui/form' +import { TelemetryActions } from 'common/telemetry-constants' import { urlRegex } from 'components/interfaces/Auth/Auth.constants' import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -15,7 +17,6 @@ import { CronJob, useCronJobsQuery } from 'data/database-cron-jobs/database-cron import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { TelemetryActions } from 'lib/constants/telemetry' import { Button, Form_Shadcn_, @@ -49,6 +50,7 @@ import { HTTPHeaderFieldsSection } from './HttpHeaderFieldsSection' import { HttpRequestSection } from './HttpRequestSection' import { SqlFunctionSection } from './SqlFunctionSection' import { SqlSnippetSection } from './SqlSnippetSection' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' export interface CreateCronJobSheetProps { selectedCronJob?: Pick @@ -77,6 +79,8 @@ const edgeFunctionSchema = z.object({ return false } }, 'Input must be valid JSON'), + // When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it + snippet: z.string().trim(), }) const httpRequestSchema = z.object({ @@ -103,12 +107,16 @@ const httpRequestSchema = z.object({ return false } }, 'Input must be valid JSON'), + // When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it + snippet: z.string().trim(), }) const sqlFunctionSchema = z.object({ type: z.literal('sql_function'), schema: z.string().trim().min(1, 'Please select one of the listed database schemas'), functionName: z.string().trim().min(1, 'Please select one of the listed database functions'), + // When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it + snippet: z.string().trim(), }) const sqlSnippetSchema = z.object({ type: z.literal('sql_snippet'), @@ -160,6 +168,30 @@ export type CronJobType = CreateCronJobForm['values'] const FORM_ID = 'create-cron-job-sidepanel' +const buildCommand = (values: CronJobType) => { + let command = '' + if (values.type === 'edge_function') { + command = buildHttpRequestCommand( + values.method, + values.edgeFunctionName, + values.httpHeaders, + values.httpBody, + values.timeoutMs + ) + } else if (values.type === 'http_request') { + command = buildHttpRequestCommand( + values.method, + values.endpoint, + values.httpHeaders, + values.httpBody, + values.timeoutMs + ) + } else if (values.type === 'sql_function') { + command = `SELECT ${values.schema}.${values.functionName}()` + } + return command +} + export const CreateCronJobSheet = ({ selectedCronJob, supportsSeconds, @@ -168,6 +200,7 @@ export const CreateCronJobSheet = ({ onClose, }: CreateCronJobSheetProps) => { const { project } = useProjectContext() + const org = useSelectedOrganization() const isEditing = !!selectedCronJob?.jobname const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) @@ -185,7 +218,7 @@ export const CreateCronJobSheet = ({ 'extensions' ) - const cronJobValues = parseCronJobCommand(selectedCronJob?.command || '') + const cronJobValues = parseCronJobCommand(selectedCronJob?.command || '', project?.ref!) const form = useForm({ resolver: zodResolver(FormSchema), @@ -212,6 +245,61 @@ export const CreateCronJobSheet = ({ } } + const [ + cronType, + endpoint, + edgeFunctionName, + method, + httpHeaders, + httpBody, + timeoutMs, + schema, + functionName, + ] = useWatch({ + control: form.control, + name: [ + 'values.type', + 'values.endpoint', + 'values.edgeFunctionName', + 'values.method', + 'values.httpHeaders', + 'values.httpBody', + 'values.timeoutMs', + 'values.schema', + 'values.functionName', + ], + }) + + // update the snippet field when the user changes the any values in the form + useEffect(() => { + const command = buildCommand({ + type: cronType, + method, + edgeFunctionName, + timeoutMs, + httpHeaders, + httpBody, + functionName, + schema, + endpoint, + snippet: '', + }) + if (command) { + form.setValue('values.snippet', command) + } + }, [ + edgeFunctionName, + endpoint, + method, + // for some reason, the httpHeaders are not memoized and cause the useEffect to trigger even when the value is the same + JSON.stringify(httpHeaders), + httpBody, + timeoutMs, + schema, + functionName, + form, + ]) + const onSubmit: SubmitHandler = async ({ name, schedule, values }) => { // job names should be unique const nameExists = cronJobs?.some( @@ -225,28 +313,7 @@ export const CreateCronJobSheet = ({ return } - let command = '' - if (values.type === 'edge_function') { - command = buildHttpRequestCommand( - values.method, - values.edgeFunctionName, - values.httpHeaders, - values.httpBody, - values.timeoutMs - ) - } else if (values.type === 'http_request') { - command = buildHttpRequestCommand( - values.method, - values.endpoint, - values.httpHeaders, - values.httpBody, - values.timeoutMs - ) - } else if (values.type === 'sql_function') { - command = `'CALL ${values.schema}.${values.functionName}()'` - } else { - command = `$$${values.snippet}$$` - } + let command = `$$${values.snippet}$$` const query = buildCronQuery(name, schedule, command) @@ -271,6 +338,10 @@ export const CreateCronJobSheet = ({ type: values.type, schedule: schedule, }, + groups: { + project: project?.ref ?? 'Unknown', + organization: org?.slug ?? 'Unknown', + }, }) } else { sendEvent({ @@ -279,6 +350,10 @@ export const CreateCronJobSheet = ({ type: values.type, schedule: schedule, }, + groups: { + project: project?.ref ?? 'Unknown', + organization: org?.slug ?? 'Unknown', + }, }) } @@ -296,8 +371,6 @@ export const CreateCronJobSheet = ({ const pgNetExtension = (data ?? []).find((ext) => ext.name === 'pg_net') const pgNetExtensionInstalled = pgNetExtension?.installed_version != undefined - const cronType = form.watch('values.type') - return ( <>
      @@ -345,7 +418,7 @@ export const CreateCronJobSheet = ({ name="function_type" value={field.value} disabled={field.disabled} - onValueChange={field.onChange} + onValueChange={(value) => field.onChange(value)} > {CRONJOB_DEFINITIONS.map((definition) => ( { const { ref } = useParams() + const org = useSelectedOrganization() const { project: selectedProject } = useProjectContext() const [toggleConfirmationModalShown, showToggleConfirmationModal] = useState(false) @@ -60,7 +62,14 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard
      - {job.jobname} + + {job.jobname ?? 'No name provided'} +
      {isLoading ? ( @@ -86,6 +95,10 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard onClick={() => { sendEvent({ action: TelemetryActions.CRON_JOB_HISTORY_CLICKED, + groups: { + project: selectedProject?.ref ?? 'Unknown', + organization: org?.slug ?? 'Unknown', + }, }) }} > @@ -100,6 +113,10 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard onClick={() => { sendEvent({ action: TelemetryActions.CRON_JOB_UPDATE_CLICKED, + groups: { + project: selectedProject?.ref ?? 'Unknown', + organization: org?.slug ?? 'Unknown', + }, }) onEditCronJob(job) }} @@ -111,6 +128,10 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard onClick={() => { sendEvent({ action: TelemetryActions.CRON_JOB_DELETE_CLICKED, + groups: { + project: selectedProject?.ref ?? 'Unknown', + organization: org?.slug ?? 'Unknown', + }, }) onDeleteCronJob(job) }} @@ -184,7 +205,15 @@ export const CronJobCard = ({ job, onEditCronJob, onDeleteCronJob }: CronJobCard
      Command
      - {[job.command.trim()]} + code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap min-h-11' + )} + />
      diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts index 5108f345ce0..c85514b75d8 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts @@ -3,83 +3,153 @@ import { cronPattern, parseCronJobCommand, secondsPattern } from './CronJobs.uti describe('parseCronJobCommand', () => { it('should return a default object when the command is null', () => { - expect(parseCronJobCommand('')).toMatchObject({ snippet: '', type: 'sql_snippet' }) + expect(parseCronJobCommand('', 'random_project_ref')).toStrictEqual({ + httpBody: '', + method: 'POST', + snippet: '', + timeoutMs: 1000, + type: 'sql_snippet', + }) }) it('should return a default object when the command is random', () => { const command = 'some random text' - expect(parseCronJobCommand(command)).toMatchObject({ + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ snippet: 'some random text', type: 'sql_snippet', }) }) - it('should return a sql function command when the command is CALL auth.jwt ()', () => { - const command = 'CALL auth.jwt ()' - expect(parseCronJobCommand(command)).toMatchObject({ + it('should return a sql function command when the command is SELECT auth.jwt ()', () => { + const command = 'SELECT auth.jwt ()' + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ type: 'sql_function', schema: 'auth', functionName: 'jwt', + snippet: command, }) }) - it('should return a edge function config when the command posts to supabase.co', () => { - const command = `select net.http_post( url:='https://_.supabase.co/functions/v1/_', headers:=jsonb_build_object(), body:='', timeout_milliseconds:=5000 );` - expect(parseCronJobCommand(command)).toMatchObject({ - edgeFunctionName: 'https://_.supabase.co/functions/v1/_', + it('should return a sql function command when the command is SELECT public.test_fn(1, 2)', () => { + const command = 'SELECT public.test_fn(1, 2)' + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ + type: 'sql_function', + schema: 'public', + functionName: 'test_fn', + snippet: command, + }) + }) + + it('should return a edge function config when the command posts to its own supabase.co project', () => { + const command = `select net.http_post( url:='https://random_project_ref.supabase.co/functions/v1/_', headers:=jsonb_build_object(), body:='', timeout_milliseconds:=5000 );` + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ + edgeFunctionName: 'https://random_project_ref.supabase.co/functions/v1/_', method: 'POST', httpHeaders: [], httpBody: '', timeoutMs: 5000, type: 'edge_function', + snippet: command, + }) + }) + + it('should return a HTTP request config when the command posts to another supabase.co project', () => { + const command = `select net.http_post( url:='https://another_project_ref.supabase.co/functions/v1/_', headers:=jsonb_build_object(), body:='', timeout_milliseconds:=5000 );` + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ + endpoint: 'https://another_project_ref.supabase.co/functions/v1/_', + method: 'POST', + httpHeaders: [], + httpBody: '', + timeoutMs: 5000, + type: 'http_request', + snippet: command, }) }) it('should return a HTTP request config with POST method, empty headers and a body as string', () => { const command = `select net.http_post( url:='https://example.com/api/endpoint', headers:=jsonb_build_object(), body:='hello', timeout_milliseconds:=5000 );` - expect(parseCronJobCommand(command)).toMatchObject({ + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ endpoint: 'https://example.com/api/endpoint', method: 'POST', httpHeaders: [], httpBody: 'hello', timeoutMs: 5000, type: 'http_request', + snippet: command, }) }) it('should return a HTTP request config with POST method, some headers and empty body', () => { - const command = `select net.http_post( url:='https://example.com/api/endpoint', headers:=jsonb_build_object('headerche', '2'), body:='', timeout_milliseconds:=1000 );` - expect(parseCronJobCommand(command)).toMatchObject({ + const command = `select net.http_post( url:='https://example.com/api/endpoint', headers:=jsonb_build_object('fst', '1', 'snd', '2'), body:='', timeout_milliseconds:=1000 );` + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ endpoint: 'https://example.com/api/endpoint', method: 'POST', - httpHeaders: [{ name: 'headerche', value: '2' }], + httpHeaders: [ + { name: 'fst', value: '1' }, + { name: 'snd', value: '2' }, + ], httpBody: '', timeoutMs: 1000, type: 'http_request', + snippet: command, }) }) it('should return a HTTP request config with GET method and empty body', () => { const command = `select net.http_get( url:='https://example.com/api/endpoint', headers:=jsonb_build_object(), timeout_milliseconds:=5000 );` - expect(parseCronJobCommand(command)).toMatchObject({ + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ endpoint: 'https://example.com/api/endpoint', method: 'GET', httpHeaders: [], httpBody: '', timeoutMs: 5000, type: 'http_request', + snippet: command, }) }) it('should return a HTTP request config with POST method and a body as JSON object', () => { const command = `select net.http_post( url:='https://example.com/api/endpoint', headers:=jsonb_build_object(), body:='{"key": "value"}', timeout_milliseconds:=5000 );` - expect(parseCronJobCommand(command)).toMatchObject({ + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ endpoint: 'https://example.com/api/endpoint', method: 'POST', httpHeaders: [], httpBody: '{"key": "value"}', timeoutMs: 5000, type: 'http_request', + snippet: command, + }) + }) + + it('should return a HTTP request config with POST method, plain JSON headers and plain JSON body', () => { + const command = `select net.http_post( url:='https://example.com/api/endpoint', headers:='{"fst": "1", "snd": "2"}',body:='{"key": "value"}',timeout_milliseconds:=5000);` + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ + endpoint: 'https://example.com/api/endpoint', + method: 'POST', + httpHeaders: [ + { name: 'fst', value: '1' }, + { name: 'snd', value: '2' }, + ], + httpBody: '{"key": "value"}', + timeoutMs: 5000, + type: 'http_request', + snippet: command, + }) + }) + + it('should return a HTTP request config with POST method, plain JSON headers and plain JSON body with ::jsonb typecasting', () => { + const command = `select net.http_post( url:='https://example.com/api/endpoint', headers:='{"fst": "1", "snd": "2"}'::jsonb,body:='{"key": "value"}'::jsonb,timeout_milliseconds:=5000);` + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ + endpoint: 'https://example.com/api/endpoint', + method: 'POST', + httpHeaders: [ + { name: 'fst', value: '1' }, + { name: 'snd', value: '2' }, + ], + httpBody: '{"key": "value"}', + timeoutMs: 5000, + type: 'http_request', + snippet: command, }) }) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts index 1fe6de9099b..10f77fcd892 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts @@ -16,18 +16,16 @@ export const buildHttpRequestCommand = ( body: string | undefined, timeout: number ) => { - return `$$ - select - net.${method === 'GET' ? 'http_get' : 'http_post'}( - url:='${url}', - headers:=jsonb_build_object(${headers - .filter((v) => v.name && v.value) - .map((v) => `'${v.name}', '${v.value}'`) - .join(', ')}), - ${method === 'POST' && body ? `body:='${body}',` : ''} - timeout_milliseconds:=${timeout} - ); - $$` + return ` +select + net.${method === 'GET' ? 'http_get' : 'http_post'}( + url:='${url}', + headers:=jsonb_build_object(${headers + .filter((v) => v.name && v.value) + .map((v) => `'${v.name}', '${v.value}'`) + .join(', ')}), ${method === 'POST' && body ? `\n body:='${body}',` : ''} + timeout_milliseconds:=${timeout} + );` } const DEFAULT_CRONJOB_COMMAND = { @@ -39,7 +37,7 @@ const DEFAULT_CRONJOB_COMMAND = { httpBody: '', } as const -export const parseCronJobCommand = (originalCommand: string): CronJobType => { +export const parseCronJobCommand = (originalCommand: string, projectRef: string): CronJobType => { const command = originalCommand .replaceAll('$$', ' ') .replaceAll(/\n/g, ' ') @@ -47,58 +45,73 @@ export const parseCronJobCommand = (originalCommand: string): CronJobType => { .trim() if (command.toLocaleLowerCase().startsWith('select net.')) { - let matches = - command.match( - /select net\.([^']+)\(\s*url:='([^']+)',\s*headers:=jsonb_build_object\(([^)]*)\),(?:\s*body:='(.*)',)?\s*timeout_milliseconds:=(\d+) \)/i - ) || [] + const methodMatch = command.match(/select net\.([^']+)\(\s*url:=/i) + const method = methodMatch?.[1] || '' - // if the match has been unsuccessful, the cron may be created with the previous encoding/parsing. - if (matches.length === 0) { - matches = - command.match( - /select net\.([^']+)\(\s*url:='([^']+)',\s*headers:=jsonb_build_object\(([^)]*)\),\s*body:=jsonb_build_object\(([^]*)\s*\),\s*timeout_milliseconds:=(\d+) \)/i - ) || [] - } + const urlMatch = command.match(/url:='([^']+)'/i) + const url = urlMatch?.[1] || '' - // convert the header string to array of objects, clean up the values, trim them of spaces and remove the quotation marks at start and end - const headers = (matches[3] || '').split(',').map((s) => s.trim().replace(/^'|'$/g, '')) - const headersObjs: { name: string; value: string }[] = [] - for (let i = 0; i < headers.length; i += 2) { - if (headers[i] && headers[i].length > 0) { - headersObjs.push({ name: headers[i], value: headers[i + 1] }) + const bodyMatch = command.match(/body:='(.*)'/i) + const body = bodyMatch?.[1] || '' + + const timeoutMatch = command.match(/timeout_milliseconds:=(\d+)/i) + const timeout = timeoutMatch?.[1] || '' + + const headersJsonBuildObjectMatch = command.match(/headers:=jsonb_build_object\(([^)]*)/i) + const headersJsonBuildObject = headersJsonBuildObjectMatch?.[1] || '' + + let headersObjs: { name: string; value: string }[] = [] + if (headersJsonBuildObject) { + // convert the header string to array of objects, clean up the values, trim them of spaces and remove the quotation marks at start and end + const headers = headersJsonBuildObject.split(',').map((s) => s.trim().replace(/^'|'$/g, '')) + + for (let i = 0; i < headers.length; i += 2) { + if (headers[i] && headers[i].length > 0) { + headersObjs.push({ name: headers[i], value: headers[i + 1] }) + } + } + } else { + const headersStringMatch = command.match(/headers:='([^']*)'/i) + const headersString = headersStringMatch?.[1] || '' + try { + const parsedHeaders = JSON.parse(headersString) + headersObjs = Object.entries(parsedHeaders).map(([name, value]) => ({ + name, + value: value as string, + })) + } catch (error) { + console.error('Error parsing headers:', error) } } - const url = matches[2] || '' - const body = matches[4] || '' - - if (url.includes('.supabase.') && url.includes('/functions/v1/')) { + if (url.includes(`${projectRef}.supabase.`) && url.includes('/functions/v1/')) { return { type: 'edge_function', - method: matches[1] === 'http_get' ? 'GET' : 'POST', + method: method === 'http_get' ? 'GET' : 'POST', edgeFunctionName: url, httpHeaders: headersObjs, httpBody: body, - // @ts-ignore - timeoutMs: +matches[5] ?? 1000, + timeoutMs: Number(timeout ?? 1000), + snippet: originalCommand, } } return { type: 'http_request', - method: matches[1] === 'http_get' ? 'GET' : 'POST', + method: method === 'http_get' ? 'GET' : 'POST', endpoint: url, httpHeaders: headersObjs, httpBody: body, - // @ts-ignore - timeoutMs: +matches[5] ?? 1000, + timeoutMs: Number(timeout ?? 1000), + snippet: originalCommand, } } - if (command.toLocaleLowerCase().startsWith('call ')) { + const regexDBFunction = /select\s+[a-zA-Z-_]*\.?[a-zA-Z-_]*\s*\(.+/g + if (command.toLocaleLowerCase().match(regexDBFunction)) { const [schemaName, functionName] = command - .replace('CALL ', '') - .replace('()', '') + .replace('SELECT ', '') + .replace(/\(.*\)/, '') .trim() .split('.') @@ -106,6 +119,7 @@ export const parseCronJobCommand = (originalCommand: string): CronJobType => { type: 'sql_function', schema: schemaName, functionName: functionName, + snippet: originalCommand, } } diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx index 96952fd468e..68772da0d66 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.tsx @@ -2,16 +2,17 @@ import { Search } from 'lucide-react' import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' import { useState } from 'react' +import { TelemetryActions } from 'common/telemetry-constants' import { CreateCronJobSheet } from 'components/interfaces/Integrations/CronJobs/CreateCronJobSheet' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { CronJob, useCronJobsQuery } from 'data/database-cron-jobs/database-cron-jobs-query' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { TelemetryActions } from 'lib/constants/telemetry' import { Button, Input, Sheet, SheetContent } from 'ui' import { CronJobCard } from './CronJobCard' import { DeleteCronJob } from './DeleteCronJob' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' const EMPTY_CRON_JOB = { jobname: '', @@ -22,6 +23,7 @@ const EMPTY_CRON_JOB = { export const CronjobsTab = () => { const { project } = useProjectContext() + const org = useSelectedOrganization() const [searchQuery, setSearchQuery] = useQueryState('search', parseAsString.withDefault('')) const [createCronJobSheetShown, setCreateCronJobSheetShown] = useQueryState( @@ -60,16 +62,22 @@ export const CronjobsTab = () => {
      ) - const filteredCronJobs = (cronJobs ?? []).filter((cj) => cj?.jobname?.includes(searchQuery || '')) + const filteredCronJobs = + searchQuery.length > 0 + ? (cronJobs ?? []).filter((cj) => cj?.jobname?.includes(searchQuery || '')) + : cronJobs ?? [] const onOpenCreateJobSheet = () => { - sendEvent({ action: TelemetryActions.CRON_JOB_CREATE_CLICKED }) + sendEvent({ + action: TelemetryActions.CRON_JOB_CREATE_CLICKED, + groups: { project: project?.ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, + }) setCreateCronJobSheetShown(true) } return ( <> -
      +
      {(cronJobs ?? []).length == 0 ? (

      No cron jobs created yet

      diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx index 41e333b7dae..5d7d8dd00a2 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx @@ -1,11 +1,12 @@ import { toast } from 'sonner' +import { TelemetryActions } from 'common/telemetry-constants' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useDatabaseCronJobDeleteMutation } from 'data/database-cron-jobs/database-cron-jobs-delete-mutation' import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' -import { TelemetryActions } from 'lib/constants/telemetry' import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' interface DeleteCronJobProps { cronJob: CronJob @@ -15,11 +16,15 @@ interface DeleteCronJobProps { export const DeleteCronJob = ({ cronJob, visible, onClose }: DeleteCronJobProps) => { const { project } = useProjectContext() + const org = useSelectedOrganization() const { mutate: sendEvent } = useSendEventMutation() const { mutate: deleteDatabaseCronJob, isLoading } = useDatabaseCronJobDeleteMutation({ onSuccess: () => { - sendEvent({ action: TelemetryActions.CRON_JOB_DELETED }) + sendEvent({ + action: TelemetryActions.CRON_JOB_DELETED, + groups: { project: project?.ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, + }) toast.success(`Successfully removed cron job ${cronJob.jobname}`) onClose() }, diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx index 6b1cbf74b73..be23bd31e9a 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx @@ -16,9 +16,9 @@ import { cn, LoadingLine, SimpleCodeBlock, - Tooltip_Shadcn_, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { calculateDuration, formatDate, isSecondsFormat } from './CronJobs.utils' @@ -41,13 +41,13 @@ const cronJobColumns = [ minWidth: 200, value: (row: CronJobRun) => (
      - - + + {row.return_message} - - + + {row.return_message} - - + +
      ), }, @@ -239,8 +239,8 @@ export const PreviousRunsTab = () => {

      Command

      - - + + { {currentJobState?.command}
      - - + + { > {currentJobState?.command} - - + +
      diff --git a/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx b/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx index 5cb36d2a21c..f2e84a6f645 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx @@ -17,7 +17,11 @@ export const BuiltBySection = forwardRef, BuiltBySectionProps> if (!name && !docsUrl && !websiteUrl) return null return ( -
      +
      {name && (
      BUILT BY
      diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx index d0fff0d5b9d..cd4ce834f2b 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx @@ -50,7 +50,7 @@ export const IntegrationOverviewTab = ({
      {dependsOnExtension && ( -
      +
      { value={selectedCategory} onValueChange={(value) => setSelectedCategory(value as IntegrationCategory)} > - + {CATEGORIES.map((category) => ( { /> -
      +
      {isLoading && new Array(3) diff --git a/apps/studio/components/interfaces/Integrations/Landing/InstalledIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/InstalledIntegrations.tsx index 30c3835ef02..c1e4f30d10c 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/InstalledIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/InstalledIntegrations.tsx @@ -6,7 +6,7 @@ export const InstalledIntegrations = () => { const { installedIntegrations, error, isLoading, isSuccess, isError } = useInstalledIntegrations() return ( -
      +

      Installed integrations

      {isLoading && diff --git a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx index b2babf911f9..76fd804bd7a 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx @@ -56,7 +56,7 @@ const supabaseIntegrations: IntegrationDefinition[] = [ id: 'queues', type: 'postgres_extension' as const, requiredExtensions: ['pgmq'], - missingExtensionsAlert: , + missingExtensionsAlert: , name: `Queues`, icon: ({ className, ...props } = {}) => ( @@ -173,6 +173,7 @@ const supabaseIntegrations: IntegrationDefinition[] = [ id: 'vault', type: 'postgres_extension' as const, requiredExtensions: ['supabase_vault'], + missingExtensionsAlert: , name: `Vault`, status: 'alpha', icon: ({ className, ...props } = {}) => ( diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx index 67d28db12f4..6d7471e1e0e 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx @@ -206,7 +206,7 @@ export const QueuesSettings = () => { send_batch,{' '} read,{' '} pop, - archive, and + archive, and{' '} delete

      diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesTab.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesTab.tsx index 5cb68eff63c..d3f3b3461fc 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/QueuesTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesTab.tsx @@ -45,7 +45,7 @@ export const QueuesTab = () => { return ( <> -
      +
      {queues.length === 0 ? (
      { const relatedFunctions = getQueueFunctionsMapping(x) return ( - - + + {x} {isExposed && } - + {isExposed && ( - +

      Required for{' '} {relatedFunctions.length === 6 @@ -308,9 +302,9 @@ export const QueueSettings = ({}: QueueSettingsProps) => { {y} ))}

      - + )} - + ) })} diff --git a/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx b/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx index ccdf6feedaf..c624aae72c8 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx @@ -4,20 +4,24 @@ import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { Button } from 'ui' import { Admonition } from 'ui-patterns/admonition' -export const UpgradeDatabaseAlert = () => { +interface UpgradeDatabaseAlertProps { + minimumVersion?: string +} + +export const UpgradeDatabaseAlert = ({ minimumVersion = '15.6' }: UpgradeDatabaseAlertProps) => { const project = useSelectedProject() return (

      This integration requires the pgmq extension which is not available on this - version of Postgres. The extension is available on version 15.6.1.143 and higher. + version of Postgres. The extension is available on version {minimumVersion} and higher.

      {idx !== keys.length - 1 && } diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx index 3988d05930c..c20f6e8f842 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretRow.tsx @@ -1,4 +1,3 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import dayjs from 'dayjs' @@ -11,6 +10,9 @@ import { DropdownMenuItem, DropdownMenuTrigger, Input, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -105,9 +107,9 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
      diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx index 6988b6d20d4..c88dd085827 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx @@ -56,11 +56,11 @@ export const SecretsManagement = () => { return ( <> -
      -
      -
      +
      +
      +
      { : [] } /> -
      +
      {tab.label} - - + + - - {tab.description} - + + {tab.description} +
      diff --git a/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx b/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx index ccd9c05ded6..a4471f159cb 100644 --- a/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx +++ b/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx @@ -9,7 +9,9 @@ import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants' import { LintEntity, NoIssuesFound, lintInfoMap } from 'components/interfaces/Linter/Linter.utils' import { Lint } from 'data/lint/lint-query' import { useRouter } from 'next/router' +import { useAppStateSnapshot } from 'state/app-state' import { + AiIconAnimation, Button, ResizableHandle, ResizablePanel, @@ -41,6 +43,7 @@ const LinterDataGrid = ({ const gridRef = useRef(null) const { ref } = useParams() const router = useRouter() + const { setAiAssistantPanel } = useAppStateSnapshot() const [view, setView] = useState<'details' | 'suggestion'>('details') @@ -237,6 +240,21 @@ const LinterDataGrid = ({

      Resolve

      + void + onClickRefresh: () => void } const LinterFilters = ({ filterOptions, activeLints, + filteredLints, currentTab, filters, + isLoading, setFilters, + onClickRefresh, }: LinterFiltersProps) => { + const { ref } = useParams() + const updateFilters = (level: LINTER_LEVELS, newFilters: string[]) => { const updatedFilters = [...filters] @@ -31,11 +43,12 @@ const LinterFilters = ({ } return ( -
      +
      {LINT_TABS.map((tab) => (
      x.level === tab.id).length === 0} labelKey="name" @@ -45,6 +58,24 @@ const LinterFilters = ({ />
      ))} +
      + + +
      ) } diff --git a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 8f016a3ff55..3ca3f447120 100644 --- a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -150,7 +150,7 @@ const AuditLogs = () => { title="Organization Audit Logs are not available on Free or Pro plans" > -
      +
      Organization Audit Logs are not available on Free or Pro plans diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx index 612a56bdae2..0275aaae5f7 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx @@ -82,7 +82,7 @@ const BillingAddress = () => { return ( -
      +

      Billing Address

      This will be reflected in every upcoming invoice, past invoices are not affected diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.constants.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.constants.ts index b4e6309f826..9916fe5ad1e 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.constants.ts +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.constants.ts @@ -24,6 +24,7 @@ export const BILLING_BREAKDOWN_METRICS: Metric[] = [ anchor: 'dbSize', category: 'Database', unitName: 'GB', + tip: 'Database size refers to the actual amount of space used by all your database objects, as reported by Postgres. Free Plan projects are limited to 0.5 GB Database Size each.', }, { key: PricingMetric.EGRESS, diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx index 19ddf0ca50a..cb0489e80ba 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx @@ -39,7 +39,7 @@ const BillingBreakdown = () => { return ( -

      +

      Billing Breakdown

      Current billing cycle: {billingCycleStart.format('MMM DD')} -{' '} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx index edca29e3ee6..aedd457de6d 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx @@ -6,14 +6,7 @@ import AlertError from 'components/ui/AlertError' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrgUpcomingInvoiceQuery } from 'data/invoices/org-invoice-upcoming-query' import { formatCurrency } from 'lib/helpers' -import { - Button, - cn, - Collapsible, - Tooltip_Shadcn_, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, -} from 'ui' +import { Button, cn, Collapsible, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { billingMetricUnit, formatUsage } from '../helpers' export interface UpcomingInvoiceProps { @@ -290,11 +283,11 @@ const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { const InvoiceTooltip = ({ text, linkRef }: { text: string; linkRef?: string }) => { return ( - - + + - - + @@ -311,8 +304,8 @@ const InvoiceTooltip = ({ text, linkRef }: { text: string; linkRef?: string }) = )}

      - - + + ) } diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx index 0a4e467a763..34e0e0131ed 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx @@ -127,7 +127,7 @@ const CostControl = ({}: CostControlProps) => {

      )} -
      +
      { const { slug } = useParams() + const creditTopUpEnabled = useFlag('creditTopUp') const canReadSubscriptions = useCheckPermissions( PermissionAction.BILLING_READ, @@ -39,14 +43,13 @@ const CreditBalance = () => { return ( -
      +

      Credit Balance

      - {isCredit && You have credits available} - {isDebt && Outstanding payments}

      - Charges will be deducted from your balance first + Credits will be applied to future invoices, before charging your payment method. If your + credit balance runs out, your default payment method will be charged.

      @@ -55,29 +58,30 @@ const CreditBalance = () => { ) : ( <> - {isLoading && ( -
      - - - -
      - )} + : null}> + + + {isError && ( + + )} - {isError && ( - - )} - - {isSuccess && ( -
      - {isDebt &&

      -

      } -

      $

      -

      {balance}

      - {isCredit &&

      /credits

      } -
      - )} + {isSuccess && ( +
      + Balance +
      + {isDebt &&

      -

      } +

      $

      +

      {balance}

      + {isCredit &&

      /credits

      } +
      +
      + )} +
      +
      +
      )} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx new file mode 100644 index 00000000000..2f8a5d0de82 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx @@ -0,0 +1,364 @@ +import HCaptcha from '@hcaptcha/react-hcaptcha' +import { zodResolver } from '@hookform/resolvers/zod' +import { Elements, useStripe } from '@stripe/react-stripe-js' +import { loadStripe, PaymentIntentResult } from '@stripe/stripe-js' +import { PermissionAction, SupportCategories } from '@supabase/shared-types/out/constants' +import { useQueryClient } from '@tanstack/react-query' +import { AlertCircle, Info } from 'lucide-react' +import { useTheme } from 'next-themes' +import Link from 'next/link' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { z } from 'zod' + +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useOrganizationCreditTopUpMutation } from 'data/organizations/organization-credit-top-up-mutation' +import { subscriptionKeys } from 'data/subscriptions/keys' +import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' +import { STRIPE_PUBLIC_KEY } from 'lib/constants' +import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store' +import { + Alert_Shadcn_, + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + DialogTrigger, + Form_Shadcn_, + FormField_Shadcn_, + Input_Shadcn_, + LoadingLine, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import PaymentMethodSelection from './Subscription/PaymentMethodSelection' + +const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) + +const FORM_ID = 'credit-top-up' + +const FormSchema = z.object({ + amount: z.coerce + .number() + .gte(100, 'Amount must be between $100 - $999.') + .lte(999, 'Amount must be between $100 - $999.') + .int('Amount must be a whole number.'), + paymentMethod: z.string(), +}) + +type CreditTopUpForm = z.infer + +export const CreditTopUp = ({ slug }: { slug: string | undefined }) => { + const { resolvedTheme } = useTheme() + const queryClient = useQueryClient() + + const canTopUpCredits = useCheckPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.subscriptions' + ) + const isPermissionsLoaded = usePermissionsLoaded() + + const { + mutateAsync: topUpCredits, + isLoading: executingTopUp, + error: errorInitiatingTopUp, + } = useOrganizationCreditTopUpMutation({}) + + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + amount: 100, + paymentMethod: '', + }, + }) + + const [topUpModalVisible, setTopUpModalVisible] = useState(false) + const [paymentConfirmationLoading, setPaymentConfirmationLoading] = useState(false) + const captchaLoaded = useIsHCaptchaLoaded() + const [captchaToken, setCaptchaToken] = useState(null) + const [captchaRef, setCaptchaRef] = useState(null) + + const captchaRefCallback = useCallback((node: any) => { + setCaptchaRef(node) + }, []) + + const resetCaptcha = () => { + setCaptchaToken(null) + captchaRef?.resetCaptcha() + } + + const initHcaptcha = async () => { + if (topUpModalVisible && captchaRef && captchaLoaded) { + let token = captchaToken + + try { + if (!token) { + const captchaResponse = await captchaRef.execute({ async: true }) + token = captchaResponse?.response ?? null + setCaptchaToken(token) + return token + } + } catch (error) { + return token + } + + return token + } + } + + useEffect(() => { + initHcaptcha() + }, [topUpModalVisible, captchaRef, captchaLoaded]) + + const [paymentIntentSecret, setPaymentIntentSecret] = useState('') + const [paymentIntentConfirmation, setPaymentIntentConfirmation] = useState() + + const onSubmit: SubmitHandler = async ({ amount, paymentMethod }) => { + setPaymentIntentConfirmation(undefined) + + const token = await initHcaptcha() + + await topUpCredits( + { + slug, + amount, + payment_method_id: paymentMethod, + hcaptchaToken: token, + }, + { + onSuccess: (data) => { + if (data.status === 'succeeded') { + onSuccessfulPayment() + } else { + setPaymentIntentSecret(data.payment_intent_secret || '') + } + + resetCaptcha() + }, + } + ) + } + + const options = useMemo(() => { + return { + clientSecret: paymentIntentSecret, + appearance: { theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', labels: 'floating' }, + } as any + }, [paymentIntentSecret, resolvedTheme]) + + const onTopUpDialogVisibilityChange = (visible: boolean) => { + setTopUpModalVisible(visible) + if (!visible) { + setCaptchaRef(null) + setPaymentIntentConfirmation(undefined) + setPaymentIntentSecret('') + } + } + + const paymentIntentConfirmed = (paymentIntentConfirmation: PaymentIntentResult) => { + // Reset payment intent secret to ensure another attempt works as expected + setPaymentIntentSecret('') + setPaymentIntentConfirmation(paymentIntentConfirmation) + + if (paymentIntentConfirmation.paymentIntent?.status === 'succeeded') { + onSuccessfulPayment() + } + } + + const onSuccessfulPayment = async () => { + onTopUpDialogVisibilityChange(false) + await queryClient.invalidateQueries(subscriptionKeys.orgSubscription(slug)) + toast.success( + 'Successfully topped up balance. It may take a minute to reflect in your account.' + ) + } + + return ( +
      + onTopUpDialogVisibilityChange(open)}> + + + Top Up + + + + e.preventDefault()}> + { + // [Joshen] This is to ensure that hCaptcha popup remains clickable + if (document !== undefined) document.body.classList.add('!pointer-events-auto') + }} + onClose={() => { + if (document !== undefined) document.body.classList.remove('!pointer-events-auto') + }} + onVerify={(token) => { + setCaptchaToken(token) + if (document !== undefined) document.body.classList.remove('!pointer-events-auto') + }} + onExpire={() => { + setCaptchaToken(null) + }} + /> + + Top Up Credits + + On successful payment, an invoice will be issued and you'll be granted credits. + Credits will be applied to future invoices only and are not refundable. The topped up + credits do not expire. + + + + + + +
      + + ( + + + + )} + /> + + ( + form.setValue('paymentMethod', pm)} + selectedPaymentMethod={form.getValues('paymentMethod')} + /> + )} + /> + +

      + For larger discounted credit packages, please{' '} + + reach out. + +

      + + {paymentIntentConfirmation && paymentIntentConfirmation.error && ( + + + Error confirming payment + + {paymentIntentConfirmation.error.message} + + + )} + + {paymentIntentConfirmation?.paymentIntent && + paymentIntentConfirmation.paymentIntent.status === 'processing' && ( + + + Payment processing + + Your payment is processing and we are waiting for a confirmation from your + card issuer. If the payment goes through you'll automatically be credited. + Please check back later. + + + )} + + {errorInitiatingTopUp && ( + + + Error topping up balance + + {errorInitiatingTopUp.message} + + + )} +
      + + {!paymentIntentConfirmation?.paymentIntent && ( + + + + )} +
      +
      + {stripePromise && paymentIntentSecret && ( + + + paymentIntentConfirmed(paymentIntentConfirmation) + } + onLoadingChange={(loading) => setPaymentConfirmationLoading(loading)} + paymentMethodId={form.getValues().paymentMethod} + /> + + )} +
      +
      +
      + ) +} + +const PaymentConfirmation = ({ + paymentIntentSecret, + onPaymentIntentConfirm, + onLoadingChange, + paymentMethodId, +}: { + paymentIntentSecret: string + paymentMethodId: string + onPaymentIntentConfirm: (response: PaymentIntentResult) => void + onLoadingChange: (loading: boolean) => void +}) => { + const stripe = useStripe() + + useEffect(() => { + if (stripe && paymentIntentSecret) { + onLoadingChange(true) + stripe! + .confirmCardPayment(paymentIntentSecret, { payment_method: paymentMethodId }) + .then((res) => { + onPaymentIntentConfirm(res) + onLoadingChange(false) + }) + .catch((err) => { + onLoadingChange(false) + }) + } + }, [paymentIntentSecret, stripe]) + + return +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip.tsx index 6e44129503e..4956a78098a 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren } from 'react' -import { TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui' +import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' export interface ProjectUpdateDisabledTooltipProps { projectUpdateDisabled: boolean @@ -16,19 +16,19 @@ const ProjectUpdateDisabledTooltip = ({ const showTooltip = projectUpdateDisabled || projectNotActive return ( - - {children} + + {children} {showTooltip && ( - + {projectUpdateDisabled ? tooltip || 'Subscription changes are currently disabled. Our engineers are working on a fix.' : projectNotActive ? 'Unable to update subscription as project is currently not active' : ''} - + )} - + ) } diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx index f5ba6e1ec06..88bd4450ca3 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx @@ -1,16 +1,16 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useEffect, useState } from 'react' import { toast } from 'sonner' import AddNewPaymentMethodModal from 'components/interfaces/Billing/Payment/AddNewPaymentMethodModal' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organization-payment-methods-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { BASE_PATH } from 'lib/constants' import { getURL } from 'lib/helpers' -import { Button, Listbox } from 'ui' -import { Loader, AlertCircle, CreditCard, Plus } from 'lucide-react' +import { AlertCircle, CreditCard, Loader, Plus } from 'lucide-react' +import { Listbox } from 'ui' export interface PaymentMethodSelectionProps { selectedPaymentMethod?: string @@ -70,37 +70,28 @@ const PaymentMethodSelection = ({

      No saved payment methods

      - - - - - {!canUpdatePaymentMethods && ( - - - -
      - + } + onClick={() => setShowAddNewPaymentMethodModal(true)} + htmlType="button" + tooltip={{ + content: { + side: 'bottom', + text: !canUpdatePaymentMethods ? ( +
      + You need additional permissions to add new payment methods to this organization
      - - - )} - + ) : undefined, + }, + }} + > + Add new +
      ) : ( { return ( +
      { return ( {`${title} diff --git a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx index baf7142059c..4de9e365ee5 100644 --- a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx +++ b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx @@ -100,105 +100,107 @@ const InvoicesSettings = () => { <> - , - Date, - Amount, - Invoice number, - - Status - , - , - ]} - body={ - invoices.length === 0 ? ( - - -

      - {isLoading - ? 'Checking for invoices' - : 'No invoices for this organization yet'} -

      -
      -
      - ) : ( - <> - {invoices.map((x) => { - return ( - - - - - -

      {dayjs(x.period_end * 1000).format('MMM DD, YYYY')}

      -
      - -

      {formatCurrency(x.amount_due / 100)}

      -
      - -

      {x.number}

      -
      - - - - -
      - {x.amount_due > 0 && - [ - InvoiceStatus.UNCOLLECTIBLE, - InvoiceStatus.OPEN, - InvoiceStatus.ISSUED, - ].includes(x.status as InvoiceStatus) && ( - - )} - -
      -
      -
      - ) - })} - - -
      -

      - {isErrorCount - ? 'Failed to retrieve total number of invoices' - : `Showing ${offset + 1} to ${ - offset + invoices.length - } out of ${count} invoices`} -

      -
      -
      -
      +
      +
      , + Date, + Amount, + Invoice number, + + Status + , + , + ]} + body={ + invoices.length === 0 ? ( + + +

      + {isLoading + ? 'Checking for invoices' + : 'No invoices for this organization yet'} +

      - - ) - } - /> + ) : ( + <> + {invoices.map((x) => { + return ( + + + + + +

      {dayjs(x.period_end * 1000).format('MMM DD, YYYY')}

      +
      + +

      {formatCurrency(x.amount_due / 100)}

      +
      + +

      {x.number}

      +
      + + + + +
      + {x.amount_due > 0 && + [ + InvoiceStatus.UNCOLLECTIBLE, + InvoiceStatus.OPEN, + InvoiceStatus.ISSUED, + ].includes(x.status as InvoiceStatus) && ( + + )} + +
      +
      +
      + ) + })} + + +
      +

      + {isErrorCount + ? 'Failed to retrieve total number of invoices' + : `Showing ${offset + 1} to ${ + offset + invoices.length + } out of ${count} invoices`} +

      +
      +
      +
      +
      +
      + + ) + } + /> + )} diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx index 4cd99393ffc..5e15b6ee800 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthAppRow.tsx @@ -14,9 +14,9 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, - Tooltip_Shadcn_, + Tooltip, + TooltipContent, + TooltipTrigger, } from 'ui' export interface OAuthAppRowProps { @@ -77,8 +77,8 @@ const OAuthAppRow = ({ app, onSelectEdit, onSelectDelete }: OAuthAppRowProps) => - + {!canInviteMembers && ( - + You need additional permissions to invite a member to this organization - + )} - + @@ -407,7 +376,7 @@ export const InviteMemberButton = () => { autoFocus {...field} autoComplete="off" - disabled={isInviting || isInvitingOld} + disabled={isInviting} placeholder="Enter email address" /> @@ -451,7 +420,7 @@ export const InviteMemberButton = () => { - diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx index 2453cddf761..1c7b12b6565 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx @@ -9,8 +9,6 @@ import { useOrganizationCreateInvitationMutation } from 'data/organization-membe import { useOrganizationDeleteInvitationMutation } from 'data/organization-members/organization-invitation-delete-mutation' import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query' import { useOrganizationMemberDeleteMutation } from 'data/organizations/organization-member-delete-mutation' -import { useOrganizationMemberInviteCreateMutation } from 'data/organizations/organization-member-invite-create-mutation' -import { useOrganizationMemberInviteDeleteMutation } from 'data/organizations/organization-member-invite-delete-mutation' import { useOrganizationMembersQuery, type OrganizationMember, @@ -95,19 +93,9 @@ export const MemberActions = ({ member }: MemberActionsProps) => { toast.error(`Failed to resend invitation: ${error.message}`) }, }) - const { mutate: inviteMemberOld, isLoading: isCreatingInviteOld } = - useOrganizationMemberInviteCreateMutation({ - onSuccess: () => { - toast.success('Resent the invitation') - }, - onError: (error) => { - toast.error(`Failed to resend the invidation: ${error.message}`) - }, - }) const { mutate: deleteInvitation, isLoading: isDeletingInvite } = useOrganizationDeleteInvitationMutation() - const { mutate: deleteInvitationOld } = useOrganizationMemberInviteDeleteMutation() const isLoading = isDeletingMember || isDeletingInvite || isCreatingInvite @@ -124,44 +112,28 @@ export const MemberActions = ({ member }: MemberActionsProps) => { if (!slug) return console.error('Slug is required') if (!invitedId) return console.error('Member invited ID is required') - if (isOptedIntoProjectLevelPermissions) { - deleteInvitation( - { slug, id: invitedId, skipInvalidation: true }, - { - onSuccess: () => { - if (!member.primary_email) return toast.error('Email is required') - const projectScopedRole = projectScopedRoles.find((role) => role.id === roleId) - if (projectScopedRole !== undefined) { - const projects = (projectScopedRole?.project_ids ?? []) - .map((id) => allProjects?.find((p) => p.id === id)?.ref) - .filter(Boolean) as string[] - inviteMember({ - slug, - email: member.primary_email, - roleId: projectScopedRole.base_role_id, - projects, - }) - } else { - inviteMember({ slug, email: member.primary_email, roleId }) - } - }, - } - ) - } else { - deleteInvitationOld( - { slug, invitedId, invalidateDetail: false }, - { - onSuccess: () => { - inviteMemberOld({ + deleteInvitation( + { slug, id: invitedId, skipInvalidation: true }, + { + onSuccess: () => { + if (!member.primary_email) return toast.error('Email is required') + const projectScopedRole = projectScopedRoles.find((role) => role.id === roleId) + if (projectScopedRole !== undefined) { + const projects = (projectScopedRole?.project_ids ?? []) + .map((id) => allProjects?.find((p) => p.id === id)?.ref) + .filter(Boolean) as string[] + inviteMember({ slug, - invitedEmail: member.primary_email!, - ownerId: invitedId, - roleId, + email: member.primary_email, + roleId: projectScopedRole.base_role_id, + projects, }) - }, - } - ) - } + } else { + inviteMember({ slug, email: member.primary_email, roleId }) + } + }, + } + ) } const handleRevokeInvitation = (member: OrganizationMember) => { @@ -169,17 +141,10 @@ export const MemberActions = ({ member }: MemberActionsProps) => { if (!slug) return console.error('Slug is required') if (!invitedId) return console.error('Member invited ID is required') - if (isOptedIntoProjectLevelPermissions) { - deleteInvitation( - { slug, id: invitedId }, - { onSuccess: () => toast.success('Successfully revoked the invitation.') } - ) - } else { - deleteInvitationOld( - { slug, invitedId }, - { onSuccess: () => toast.success('Successfully revoked the invitation.') } - ) - } + deleteInvitation( + { slug, id: invitedId }, + { onSuccess: () => toast.success('Successfully revoked the invitation.') } + ) } if (!canRemoveMember || (isPendingInviteAcceptance && !canResendInvite && !canRevokeInvite)) { @@ -306,7 +271,7 @@ export const MemberActions = ({ member }: MemberActionsProps) => {

      If you'd like to retain the member's shared SQL snippets, right click on them and - "Duplicate personal copy" in the SQL Editor before removing this member. + "Duplicate query" in the SQL Editor before removing this member.

      ), diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx index b5c3f5b3be0..8a6991c5425 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx @@ -72,9 +72,9 @@ export const MemberRow = ({ member }: MemberRowProps) => {
      -
      +
      {isInvitedUser || isEmailUser || isFlyUser || hasInvalidImg ? ( -
      +
      ) : ( @@ -83,7 +83,7 @@ export const MemberRow = ({ member }: MemberRowProps) => { src={`https://github.com/${member.username}.png?size=80`} width="40" height="40" - className="border rounded-full" + className="border rounded-full w-[32px] h-[32px] md:w-[40px] md:h-[40px]" onError={() => { setHasInvalidImg(true) }} diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx index 0a6e6549056..272cd47bdb0 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx @@ -7,15 +7,9 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query' import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query' import { useProfile } from 'lib/profile' -import { - Button, - Loading, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, - Tooltip_Shadcn_, -} from 'ui' -import { MemberRow } from './MemberRow' +import { Button, Loading, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { Admonition } from 'ui-patterns' +import { MemberRow } from './MemberRow' export interface MembersViewProps { searchString: string @@ -80,7 +74,7 @@ const MembersView = ({ searchString }: MembersViewProps) => { )} {isSuccessMembers && ( -
      +
      { , Role - - + + - - - How to configure access control? - - + + How to configure access control? + , , ]} diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx index 4140d5e66af..8bd83885993 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx @@ -79,18 +79,19 @@ const TeamSettings = () => { <> - + } size="small" + className="w-full md:w-auto" value={searchString} onChange={(e: any) => setSearchString(e.target.value)} name="email" id="email" placeholder="Filter members" /> - + {organizationMembersCreationEnabled && canAddMembers && profile !== undefined && diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx index 77da13e4978..93091c695d6 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.tsx @@ -36,9 +36,9 @@ import { SheetHeader, SheetSection, Switch, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, - Tooltip_Shadcn_, + Tooltip, + TooltipContent, + TooltipTrigger, WarningIcon, cn, } from 'ui' @@ -243,16 +243,16 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
      {cannotAddAnyRoles ? ( - - + +
      {role?.name ?? 'Unknown'}
      -
      - + + Additional permissions required to update role - -
      + + ) : ( - + +
      diff --git a/apps/studio/components/interfaces/Organization/Usage/SectionContent.tsx b/apps/studio/components/interfaces/Organization/Usage/SectionContent.tsx index 6671146d031..ebabba92a80 100644 --- a/apps/studio/components/interfaces/Organization/Usage/SectionContent.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/SectionContent.tsx @@ -20,7 +20,7 @@ const SectionContent = ({ return ( <> -
      +
      diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.constants.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.constants.tsx index 49c11f00cef..3daecfb854b 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.constants.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.constants.tsx @@ -2,7 +2,7 @@ import { USAGE_APPROACHING_THRESHOLD } from 'components/interfaces/Billing/Billi import { EgressType, PricingMetric } from 'data/analytics/org-daily-stats-query' import type { OrgSubscription } from 'data/subscriptions/types' import type { OrgUsageResponse } from 'data/usage/org-usage-query' -import { Alert } from 'ui' +import { Admonition } from 'ui-patterns' export const COLOR_MAP = { white: { bar: 'fill-foreground', marker: 'bg-foreground' }, @@ -109,7 +109,7 @@ export const USAGE_CATEGORIES: (subscription?: OrgSubscription) => CategoryMeta[ chartPrefix: 'Average', unit: 'bytes', description: - 'Database size refers to the monthly average database space usage, as reported by Postgres. Paid Plans use auto-scaling disks.\nBilling is based on the average daily database size used in GB throughout the billing period. Billing is independent of the provisioned disk size.', + 'Database size refers to the actual amount of space used by all your database objects, as reported by Postgres.', links: [ { name: 'Documentation', @@ -134,9 +134,8 @@ export const USAGE_CATEGORIES: (subscription?: OrgSubscription) => CategoryMeta[ return (
      {(isApproachingLimit || isExceededLimit) && isCapped && ( - CategoryMeta[ : "Disable your spend cap to scale seamlessly, and pay for over-usage beyond your Plan's quota."}
      - + )}
      ) diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx index a89a42fba20..e576ec79e85 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx @@ -4,8 +4,8 @@ import { useEffect, useMemo, useState } from 'react' import { useParams } from 'common' import { ScaffoldContainer, ScaffoldContainerLegacy } from 'components/layouts/Scaffold' -import DateRangePicker from 'components/to-be-cleaned/DateRangePicker' import AlertError from 'components/ui/AlertError' +import { DateRangePicker } from 'components/ui/DateRangePicker' import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' @@ -113,7 +113,7 @@ const Usage = () => { return ( <> -
      +
      {isLoadingSubscription && } {isErrorSubscription && ( @@ -138,7 +138,7 @@ const Usage = () => { { if (value === 'all-projects') setSelectedProjectRef(undefined) diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx index ca23fe4c30d..517b5ae00f4 100644 --- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx @@ -1,4 +1,3 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import { AlertTriangle, BarChart2 } from 'lucide-react' import Link from 'next/link' import { useMemo } from 'react' @@ -11,7 +10,7 @@ import type { OrgSubscription } from 'data/subscriptions/types' import type { OrgMetricsUsage, OrgUsageResponse } from 'data/usage/org-usage-query' import { USAGE_APPROACHING_THRESHOLD } from 'lib/constants' import type { ResponseError } from 'types' -import { Button, cn } from 'ui' +import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import SectionContent from '../SectionContent' import { CategoryAttribute } from '../Usage.constants' import { @@ -101,8 +100,8 @@ const AttributeUsage = ({

      {attribute.name} usage

      {showUsageWarning && ( - - + + {usageRatio >= 1 ? (
      ) )} - - - - -
      -

      - Exceeding your plans included usage will lead to restrictions to - your project. -

      -

      - Upgrade to a usage-based plan or disable the spend cap to avoid - restrictions. -

      -
      -
      -
      - + + +

      + Exceeding your plans included usage will lead to restrictions to + your project. +

      +

      + Upgrade to a usage-based plan or disable the spend cap to avoid + restrictions. +

      +
      + )}
      diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DatabaseSizeUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DatabaseSizeUsage.tsx new file mode 100644 index 00000000000..76129eeb3cf --- /dev/null +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DatabaseSizeUsage.tsx @@ -0,0 +1,164 @@ +import AlertError from 'components/ui/AlertError' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import type { OrgSubscription } from 'data/subscriptions/types' +import SectionContent from '../SectionContent' +import { CategoryAttribute } from '../Usage.constants' +import { useOrgProjectsQuery } from 'data/projects/org-projects' +import { PROJECT_STATUS } from 'lib/constants' +import { + Button, + Alert_Shadcn_, + CriticalIcon, + AlertTitle_Shadcn_, + AlertDescription_Shadcn_, +} from 'ui' +import MotionNumber from '@number-flow/react' +import Link from 'next/link' +import { useMemo } from 'react' +import { InfoTooltip } from 'ui-patterns/info-tooltip' +import { OrgUsageResponse } from 'data/usage/org-usage-query' +import { PricingMetric } from 'data/analytics/org-daily-stats-query' +import Panel from 'components/ui/Panel' +import { formatBytes } from 'lib/helpers' + +export interface DatabaseSizeUsageProps { + slug: string + projectRef?: string + attribute: CategoryAttribute + subscription?: OrgSubscription + usage?: OrgUsageResponse + + currentBillingCycleSelected: boolean +} + +const DatabaseSizeUsage = ({ + attribute, + subscription, + usage, + currentBillingCycleSelected, +}: DatabaseSizeUsageProps) => { + const databaseSizeUsage = useMemo( + () => usage?.usages.find((it) => it.metric === PricingMetric.DATABASE_SIZE), + [usage] + ) + + const hasProjectsExceedingDatabaseSize = useMemo(() => { + return databaseSizeUsage?.project_allocations.some((it) => it.usage > 0.5 * 1e9) + }, [databaseSizeUsage]) + + return ( +
      + +
      + {currentBillingCycleSelected && hasProjectsExceedingDatabaseSize && ( + + + Projects exceeding quota + + You have projects that are exceeding 0.5 GB of database size. Reduce the database + size or upgrade to a paid plan. + + + )} + +
      +
      +
      +

      {attribute.name} usage

      +
      +
      + +
      +

      + Included in {subscription?.plan?.name} Plan +

      +

      0.5 GB per project

      +
      + +
      +

      Max database size

      +

      + {databaseSizeUsage?.usage ? formatBytes(databaseSizeUsage?.usage_original) : '-'} +

      +
      +
      + + {currentBillingCycleSelected ? ( +
      +
      +

      Current database size per project

      +
      + + {databaseSizeUsage?.project_allocations.length === 0 && ( + + +
      +

      No active projects

      +

      + You don't have any active projects in this organization. +

      +
      +
      +
      + )} + + {databaseSizeUsage?.project_allocations.map((project, idx) => { + return ( +
      +
      + + {project.name} + + +
      +
      +
      + + + {formatBytes(project.usage)} + {' '} + Database Size + + +

      + {formatBytes(project.usage)} GB database size as reported by Postgres. +

      +
      +
      +
      +
      + ) + })} +
      + ) : ( + + +
      +

      Data not available

      +

      + Switch to current billing cycle to see current database size per project. +

      +
      +
      +
      + )} +
      +
      +
      + ) +} + +export default DatabaseSizeUsage diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx index 94ecf0119a6..0ac82809832 100644 --- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/DiskUsage.tsx @@ -146,7 +146,7 @@ const DiskUsage = ({

      Current disk size per project

      Breakdown of disk per project. Head to your project's disk management section to - see database space used. + see database size used.

      diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx index 9c5b29e4707..0de7ab646da 100644 --- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/UsageSection.tsx @@ -6,6 +6,8 @@ import SectionHeader from '../SectionHeader' import { CategoryMetaKey, USAGE_CATEGORIES } from '../Usage.constants' import AttributeUsage from './AttributeUsage' import DiskUsage from './DiskUsage' +import { PricingMetric } from 'data/analytics/org-daily-stats-query' +import DatabaseSizeUsage from './DatabaseSizeUsage' export interface ChartMeta { [key: string]: { data: DataPoint[]; margin: number; isLoading: boolean } @@ -60,6 +62,16 @@ const UsageSection = ({ currentBillingCycleSelected={currentBillingCycleSelected} usage={usage} /> + ) : attribute.key === PricingMetric.DATABASE_SIZE ? ( + ) : ( + layout?: 'vertical' | 'horizontal' + collapsible?: boolean } -export const AdvancedConfiguration = ({ form }: AdvancedConfigurationProps) => { - return ( - - - - Advanced Configuration - - - - ( - <> - { + const content = ( + <> + ( + <> + + + field.onChange(value === 'true')} + defaultValue={field.value.toString()} > - - field.onChange(value === 'true')} - defaultValue={field.value.toString()} - > - - - - Postgres - - Default - - - } - description="Recommended for production workloads" - className="[&>div>div>p]:text-left [&>div>div>p]:text-xs" - /> - - - - - - Postgres with OrioleDB - - Alpha - - - } - description="Not recommended for production workloads" - className={cn( - '[&>div>div>p]:text-left [&>div>div>p]:text-xs', - form.getValues('useOrioleDb') ? '!rounded-b-none' : '' - )} - /> - - - - - {form.getValues('useOrioleDb') && ( - - + + + Postgres + + Default + + + } + description="Recommended for production workloads" + className="[&>div>div>p]:text-left [&>div>div>p]:text-xs" /> - - )} - - - )} - /> - -

      - These settings cannot be changed after the project is created -

      -
      -
      -
      + + + + + + Postgres with OrioleDB + + Alpha + + + } + description="Not recommended for production workloads" + className={cn( + '[&>div>div>p]:text-left [&>div>div>p]:text-xs', + form.getValues('useOrioleDb') ? '!rounded-b-none' : '' + )} + /> + + + + + {form.getValues('useOrioleDb') && ( + + + + )} + + + )} + /> +

      + These settings cannot be changed after the project is created +

      + ) + + const collapsibleContent = ( + + + Advanced Configuration + + + + {content} + + + ) + + return {collapsible ? collapsibleContent : content} } diff --git a/apps/studio/components/interfaces/ProjectCreation/InitialStep.tsx b/apps/studio/components/interfaces/ProjectCreation/InitialStep.tsx new file mode 100644 index 00000000000..4c13387d69d --- /dev/null +++ b/apps/studio/components/interfaces/ProjectCreation/InitialStep.tsx @@ -0,0 +1,82 @@ +import { Database, Import } from 'lucide-react' + +import { cn } from 'ui' +import { SchemaGenerator } from './SchemaGenerator' + +interface SupabaseService { + name: 'Auth' | 'Storage' | 'Database' | 'Edge Function' | 'Cron' | 'Queues' | 'Vector' + reason: string +} + +export const InitialStep = ({ + onSubmit, + onStartBlank, + onMigrate, + onSqlGenerated, + onServicesUpdated, + onTitleUpdated, +}: { + onSubmit: (value: string) => void + onStartBlank: () => void + onMigrate: () => void + onSqlGenerated: (sql: string) => void + onServicesUpdated: (services: SupabaseService[]) => void + onTitleUpdated: (title: string) => void +}) => { + return ( +
      +

      What are you building?

      +

      + We can generate a schema for you to kick start your project. +

      + { + onSqlGenerated(sql) + onSubmit(sql) + }} + onServicesUpdated={onServicesUpdated} + onTitleUpdated={onTitleUpdated} + /> +
      + or +
      +
      +
      + +
      + Start blank + Configure a database and dive right in +
      +
      +
      + +
      + Migrate + + Import your database from another provider + +
      +
      +
      +
      + ) +} diff --git a/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx index c0b550643b3..04ca3bcf2e0 100644 --- a/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx @@ -32,6 +32,7 @@ interface PostgresVersionSelectorProps { organizationSlug: string | undefined field: ControllerRenderProps form: UseFormReturn + layout?: 'vertical' | 'horizontal' } const formatValue = ({ postgres_engine, release_channel }: ProjectCreationPostgresVersion) => { @@ -53,6 +54,7 @@ export const PostgresVersionSelector = ({ organizationSlug, field, form, + layout = 'horizontal', }: PostgresVersionSelectorProps) => { const { data, @@ -73,7 +75,7 @@ export const PostgresVersionSelector = ({ }, [isSuccess, form]) return ( - + { + return ( + prevProps.sqlStatements.length === nextProps.sqlStatements.length && + prevProps.showInfo === nextProps.showInfo && + prevProps.services.length === nextProps.services.length && + prevProps.selectedRegion?.name === nextProps.selectedRegion?.name && + prevProps.projectDetails.dbRegion === nextProps.projectDetails.dbRegion && + prevProps.projectDetails.cloudProvider === nextProps.projectDetails.cloudProvider && + prevProps.projectDetails.postgresVersion === nextProps.projectDetails.postgresVersion + ) +} + +export const ProjectVisual = memo( + ({ + sqlStatements, + showInfo = true, + services, + selectedRegion, + projectDetails, + }: ProjectVisualProps) => { + const variants = { + center: { + scale: 1, + opacity: 1, + top: '50%', + right: '50%', + x: '50%', + y: '-70%', + }, + corner: { + scale: 1, + opacity: 1, + top: '3%', + right: '3%', + x: '0%', + y: '0%', + }, + } + + const currentVariant = showInfo ? (sqlStatements.length > 0 ? 'corner' : 'center') : 'hidden' + return ( +
      +
      + {showInfo && ( + +
      +
      +
      +

      Primary Database

      +

      + + {projectDetails.dbRegion} + +

      +

      + + {projectDetails.cloudProvider} + + • + + {projectDetails.postgresVersion} + +

      +
      +
      + {selectedRegion && ( + region icon + )} +
      + + +
      + {[ + { name: 'Auth', icon: User2 }, + { name: 'Storage', icon: File }, + { name: 'Database', icon: Database }, + { name: 'Edge Function', icon: Zap }, + { name: 'Cron', icon: Clock }, + { name: 'Queues', icon: ListOrdered }, + { name: 'Vector', icon: Box }, + ].map((service) => { + const enabledService = services.find((s) => s.name === service.name) + const isEnabled = !!enabledService + return ( + + +
      + +
      +
      + + {isEnabled ? `${service.name}: ${enabledService.reason}` : service.name} + +
      + ) + })} +
      +
      +
      + )} + + + + + +
      +
      + ) + }, + arePropsEqual +) + +ProjectVisual.displayName = 'ProjectVisual' diff --git a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx index aac54d4c2f4..815081d4923 100644 --- a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx @@ -19,13 +19,18 @@ interface RegionSelectorProps { cloudProvider: CloudProvider field: ControllerRenderProps form: UseFormReturn + layout?: 'vertical' | 'horizontal' } // [Joshen] Let's use a library to maintain the flag SVGs in the future // I tried using https://flagpack.xyz/docs/development/react/ but couldn't get it to render // ^ can try again next time -export const RegionSelector = ({ cloudProvider, field }: RegionSelectorProps) => { +export const RegionSelector = ({ + cloudProvider, + field, + layout = 'horizontal', +}: RegionSelectorProps) => { const router = useRouter() const showNonProdFields = @@ -41,7 +46,7 @@ export const RegionSelector = ({ cloudProvider, field }: RegionSelectorProps) => return ( diff --git a/apps/studio/components/interfaces/ProjectCreation/SchemaFlow.tsx b/apps/studio/components/interfaces/ProjectCreation/SchemaFlow.tsx new file mode 100644 index 00000000000..31ffdc8d702 --- /dev/null +++ b/apps/studio/components/interfaces/ProjectCreation/SchemaFlow.tsx @@ -0,0 +1,86 @@ +import { useCallback, useEffect, useMemo } from 'react' +import { + applyNodeChanges, + Background, + BackgroundVariant, + Edge, + Node, + NodeChange, + NodeProps, + ReactFlow, + useEdgesState, + useNodesState, + useReactFlow, +} from 'reactflow' + +import { TableNode, TableNodeData } from '../Database/Schemas/SchemaTableNode' + +export const TABLE_NODE_WIDTH = 640 +export const TABLE_NODE_ROW_HEIGHT = 80 + +interface SchemaFlowProps { + nodes: Node[] + edges: Edge[] +} + +export const SchemaFlow = ({ nodes: initialNodes, edges: initialEdges }: SchemaFlowProps) => { + const [nodes, setNodes] = useNodesState(initialNodes) + const [edges, setEdges] = useEdgesState(initialEdges) + const nodeTypes = useMemo( + () => ({ table: (props: NodeProps) => }), + [] + ) + const reactFlowInstance = useReactFlow() + + useEffect(() => { + setNodes(initialNodes) + setEdges(initialEdges) + }, [initialNodes, initialEdges, setNodes, setEdges]) + + useEffect(() => { + reactFlowInstance.fitView() + }, [reactFlowInstance, nodes, edges]) + + const onNodesChange = useCallback( + (changes: NodeChange[]) => { + setNodes((nds) => applyNodeChanges(changes, nds)) + }, + [setNodes] + ) + + return ( +
      + + + +
      + ) +} diff --git a/apps/studio/components/interfaces/ProjectCreation/SchemaGenerator.tsx b/apps/studio/components/interfaces/ProjectCreation/SchemaGenerator.tsx new file mode 100644 index 00000000000..3b1731e059a --- /dev/null +++ b/apps/studio/components/interfaces/ProjectCreation/SchemaGenerator.tsx @@ -0,0 +1,214 @@ +import { useChat } from 'ai/react' +import { useEffect, useState } from 'react' + +import { TelemetryActions } from 'common/telemetry-constants' +import { Markdown } from 'components/interfaces/Markdown' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { BASE_PATH } from 'lib/constants' +import { AiIconAnimation, Button, Label_Shadcn_, Textarea } from 'ui' + +interface SupabaseService { + name: 'Auth' | 'Storage' | 'Database' | 'Edge Function' | 'Cron' | 'Queues' | 'Vector' + reason: string +} + +interface SchemaGeneratorProps { + step: 'initial' | 'second' + onSqlGenerated: (sql: string) => void + onReset?: () => void + onServicesUpdated: (services: SupabaseService[]) => void + onTitleUpdated: (title: string) => void + isOneOff?: boolean +} + +export const SchemaGenerator = ({ + step, + onSqlGenerated, + onServicesUpdated, + onTitleUpdated, + onReset, + isOneOff = false, +}: SchemaGeneratorProps) => { + const [input, setInput] = useState('') + const [hasSql, setHasSql] = useState(false) + + const [promptIntendSent, setPromptIntendSent] = useState(false) + const { mutate: sendEvent } = useSendEventMutation() + + const { + messages, + setMessages, + handleInputChange, + append, + isLoading: isMessagesLoading, + } = useChat({ + api: `${BASE_PATH}/api/ai/onboarding/design`, + id: 'schema-generator', + maxSteps: 7, + onFinish: () => { + setInput('') + }, + async onToolCall({ toolCall }) { + if (toolCall.toolName === 'executeSql') { + try { + const sql = (toolCall.args as { sql: string }).sql + setHasSql(true) + onSqlGenerated(sql) + return { + success: true, + message: 'Database successfully updated. Respond with next steps.', + } + } catch (error) { + console.error('Failed to execute SQL:', error) + return { + success: false, + error: `SQL execution failed: ${error instanceof Error ? error.message : String(error)}`, + } + } + } + + if (toolCall.toolName === 'reset') { + try { + setHasSql(false) + onSqlGenerated('') + if (onReset) { + onReset() + } + return { + success: true, + message: 'Database successfully reset', + } + } catch (error) { + console.error('Failed to reset the database', error) + return { + success: false, + error: `Resetting the database failed: ${error instanceof Error ? error.message : String(error)}`, + } + } + } + + if (toolCall.toolName === 'setServices') { + const newServices = (toolCall.args as { services: SupabaseService[] }).services + onServicesUpdated(newServices) + return 'Services updated successfully' + } + + if (toolCall.toolName === 'setTitle') { + const newTitle = (toolCall.args as { title: string }).title + onTitleUpdated(newTitle) + return 'Title updated successfully' + } + }, + }) + + useEffect(() => { + if (isOneOff) { + setMessages([]) + setInput('') + } + }, [isOneOff, setMessages, setInput]) + + return ( +
      + {!isOneOff && ( +
      + + Generate a starting schema + + {messages?.length > 0 && ( + + )} +
      + )} +
      + {messages.length > 0 && + messages[messages.length - 1].role === 'assistant' && + messages[messages.length - 1].content.length > 0 && + ((isOneOff && !hasSql) || !isOneOff) && ( +
      +

      + m.role === 'assistant').slice(-1)[0]?.content || '' + } + /> +

      +
      + )} +
      +