diff --git a/.github/pr-screenshots/docs-1080/pr1-hipaa-compliance.png b/.github/pr-screenshots/docs-1080/pr1-hipaa-compliance.png new file mode 100644 index 00000000000..62addb4331d Binary files /dev/null and b/.github/pr-screenshots/docs-1080/pr1-hipaa-compliance.png differ diff --git a/.github/pr-screenshots/docs-1080/pr1-logs.png b/.github/pr-screenshots/docs-1080/pr1-logs.png new file mode 100644 index 00000000000..34a907a0e89 Binary files /dev/null and b/.github/pr-screenshots/docs-1080/pr1-logs.png differ diff --git a/.github/pr-screenshots/docs-1080/pr1-postgres-connection-logging.png b/.github/pr-screenshots/docs-1080/pr1-postgres-connection-logging.png new file mode 100644 index 00000000000..3751f465602 Binary files /dev/null and b/.github/pr-screenshots/docs-1080/pr1-postgres-connection-logging.png differ diff --git a/.github/workflows/studio-e2e-test.yml b/.github/workflows/studio-e2e-test.yml index 46e85ec2ae3..73204845f78 100644 --- a/.github/workflows/studio-e2e-test.yml +++ b/.github/workflows/studio-e2e-test.yml @@ -23,7 +23,8 @@ jobs: tests_ran: ${{ steps.filter.outputs.studio == 'true' }} permissions: - contents: write + contents: read + id-token: write env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -83,6 +84,18 @@ jobs: if: steps.filter.outputs.studio == 'true' run: rm -rf supabase && pnpm exec supabase init && mkdir supabase/functions + # Authenticate with AWS ECR to avoid rate limiting + - name: configure aws credentials + if: steps.filter.outputs.studio == 'true' && !github.event.pull_request.head.repo.fork + uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 + with: + role-to-assume: ${{ secrets.PROD_AWS_ROLE }} + aws-region: us-east-1 + - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + if: steps.filter.outputs.studio == 'true' && !github.event.pull_request.head.repo.fork + with: + registry: public.ecr.aws + - name: Pre-start diagnostics run: | docker ps -a diff --git a/.prettierignore b/.prettierignore index 8c41d148ff5..66e4d58fa92 100644 --- a/.prettierignore +++ b/.prettierignore @@ -22,6 +22,9 @@ apps/**/.turbo apps/docs/CONTRIBUTING.md apps/docs/__generated__ apps/design-system/__registry__ +# TanStack Router auto-generated route tree; the file header explicitly +# says to exclude it from formatters. +apps/studio/routeTree.gen.ts packages/icons/__registry__ packages/icons/src/icons/*.ts apps/ui-library/__registry__ diff --git a/apps/design-system/content/docs/components/breadcrumb.mdx b/apps/design-system/content/docs/components/breadcrumb.mdx index 4665d2938bf..dca0f8bf821 100644 --- a/apps/design-system/content/docs/components/breadcrumb.mdx +++ b/apps/design-system/content/docs/components/breadcrumb.mdx @@ -48,7 +48,7 @@ import { BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, -} from '@/components/ui/breadcrumb' +} from 'ui' ``` ```tsx @@ -111,7 +111,7 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" +} from "ui" ... @@ -139,7 +139,7 @@ We provide a `` component to show a collapsed state when t ```tsx showLineNumbers {1,9} -import { BreadcrumbEllipsis } from "@/components/ui/breadcrumb" +import { BreadcrumbEllipsis } from "ui" ... diff --git a/apps/design-system/package.json b/apps/design-system/package.json index 5f8269c8af0..089349e2223 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "@hookform/resolvers": "^3.1.1", - "@tanstack/react-table": "^8.21.3", + "@tanstack/react-table": "catalog:", "contentlayer2": "0.4.6", "common": "workspace:*", "date-fns": "^2.30.0", diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore index eb1e923d280..5dbb96adacc 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -32,6 +32,7 @@ public/llms/ # Generated guide and reference markdown files public/markdown/ public/docs.tar.gz +public/docs/ # Copied examples folder /examples/ diff --git a/apps/docs/app/contributing/content.mdx b/apps/docs/app/contributing/content.mdx index a4c0476d5d4..be59011c43b 100644 --- a/apps/docs/app/contributing/content.mdx +++ b/apps/docs/app/contributing/content.mdx @@ -26,7 +26,6 @@ For content that requires progressive disclosure: size="medium" className="text-foreground-light mt-8 mb-6" > -
-
-
-
``` @@ -59,8 +55,7 @@ For content that requires progressive disclosure: size="medium" className="text-foreground-light mt-8 mb-6" > -
- @@ -69,9 +64,7 @@ For content that requires progressive disclosure: -
-
- @@ -80,7 +73,6 @@ For content that requires progressive disclosure: -
### Admonition @@ -277,6 +269,7 @@ You can also import the `supabase-js` library here: ````mdx ```js import { createClient } from '@supabase/supabase-js' + const supabase = createClient('dummy', 'client') // ---cut--- @@ -291,6 +284,7 @@ Note the hidden statements above the cut. Hover over `signInWithPassword` to see ```js import { createClient } from '@supabase/supabase-js' + const supabase = createClient('dummy', 'client') // ---cut--- @@ -513,8 +507,7 @@ We incorporate content reuse in the docs to avoid duplication. If you find yours size="medium" className="text-foreground-light mt-8 mb-6" > -
- @@ -527,9 +520,7 @@ We incorporate content reuse in the docs to avoid duplication. If you find yours -
-
- @@ -542,7 +533,6 @@ We incorporate content reuse in the docs to avoid duplication. If you find yours -
To make a new partial: diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index d4ce3c8084f..3b07a4cf095 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -2773,6 +2773,10 @@ export const platform: NavMenuConstant = { }, { name: 'Performance Tuning', url: '/guides/platform/performance' as `/${string}` }, { name: 'SSL Enforcement', url: '/guides/platform/ssl-enforcement' as `/${string}` }, + { + name: 'Postgres Connection Logging', + url: '/guides/platform/postgres-connection-logging' as `/${string}`, + }, { name: 'Default Platform Permissions', url: '/guides/platform/permissions' as `/${string}`, diff --git a/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.constants.ts b/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.constants.ts new file mode 100644 index 00000000000..abf52483ef9 --- /dev/null +++ b/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.constants.ts @@ -0,0 +1,30 @@ +// Shared copy for the Realtime throughput tables. Consumed both by the +// interactive component and by the markdown alternative in +// `internals/markdown-schema/RealtimeLimitsEstimator.ts`, so the headings and +// compute labels stay in sync. Keep this file free of React/browser imports. + +export const COMPUTE_OPTIONS = [ + { value: 'micro', label: 'Micro' }, + { value: 'small', label: 'Small to medium' }, + { value: 'large', label: 'Large to 16XL' }, +] as const + +export const COMPUTE_LABELS: Record = Object.fromEntries( + COMPUTE_OPTIONS.map((o) => [o.value, o.label]) +) + +// Input-parameter columns (only shown in the full/raw table). +export const THROUGHPUT_PARAM_HEADINGS = ['Filters', 'RLS', 'Connected clients'] as const + +// Result columns (shown in both the current-selection table and the raw table). +export const THROUGHPUT_METRIC_HEADINGS = [ + 'Total DB changes /sec', + 'Max messages per client /sec', + 'Max total messages /sec', + 'Latency p95', +] as const + +export const THROUGHPUT_TABLE_HEADINGS = [ + ...THROUGHPUT_PARAM_HEADINGS, + ...THROUGHPUT_METRIC_HEADINGS, +] as const diff --git a/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx b/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx index e1a3659de8e..de5402b9d3b 100644 --- a/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx +++ b/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx @@ -14,6 +14,13 @@ import { SelectValue, } from 'ui' +import { + COMPUTE_LABELS, + COMPUTE_OPTIONS, + THROUGHPUT_METRIC_HEADINGS, + THROUGHPUT_TABLE_HEADINGS, +} from './RealtimeLimitsEstimator.constants' + export default function RealtimeLimitsEstimater({}) { const findTableValue = ({ computeAddOn, filters, rls, concurrency }) => { return throughputTable.find( @@ -71,9 +78,11 @@ export default function RealtimeLimitsEstimater({}) { - Micro - Small to medium - Large to 16XL + {COMPUTE_OPTIONS.map((option) => ( + + {option.label} + + ))} @@ -129,10 +138,11 @@ export default function RealtimeLimitsEstimater({}) { - - - - + {THROUGHPUT_METRIC_HEADINGS.map((heading) => ( + + ))} @@ -174,23 +184,15 @@ export default function RealtimeLimitsEstimater({}) { .filter((v, i, a) => a.indexOf(v) === i) .map((computeAddOn) => (
-

- {computeAddOn === 'micro' - ? 'Micro' - : computeAddOn === 'small' - ? 'Small to medium' - : 'Large to 16XL'} -

+

{COMPUTE_LABELS[computeAddOn]}

Total DB changes /secMax messages per client /secMax total messages /secLatency p95 + {heading} +
- - - - - - - + {THROUGHPUT_TABLE_HEADINGS.map((heading) => ( + + ))} diff --git a/apps/docs/components/SharedData.tsx b/apps/docs/components/SharedData.tsx index 113a8340195..744be85b829 100644 --- a/apps/docs/components/SharedData.tsx +++ b/apps/docs/components/SharedData.tsx @@ -1,7 +1,8 @@ -import { at } from 'lodash-es' import { ReactNode } from 'react' import { config, logConstants } from 'shared-data' +import { resolveSharedDataPath } from './SharedData.utils' + const sharedData = { config, logConstants, @@ -25,12 +26,10 @@ function SharedData({ data: keyof typeof sharedData children: ((selectedData: (typeof sharedData)[keyof typeof sharedData]) => ReactNode) | string }) { - let selectedData = sharedData[data] as any - return typeof children === 'string' - ? ((typeof (selectedData = at(selectedData, [children])[0]) === 'object' - ? `${selectedData.value ?? ''} ${selectedData.unit ?? ''}`.trim() - : selectedData) as unknown as ReactNode) - : children(selectedData) + if (typeof children === 'string') { + return resolveSharedDataPath(sharedData[data], children) as ReactNode + } + return children(sharedData[data]) } export { SharedData } diff --git a/apps/docs/components/SharedData.utils.ts b/apps/docs/components/SharedData.utils.ts new file mode 100644 index 00000000000..512eab59cfe --- /dev/null +++ b/apps/docs/components/SharedData.utils.ts @@ -0,0 +1,19 @@ +import { at } from 'lodash-es' + +/** + * Resolves a dot/bracket path within a shared-data dataset. If the resolved + * value is an object with `value`/`unit` fields, returns `${value} ${unit}` + * (trimmed); otherwise returns the resolved primitive as-is. + * + * Pure: no `shared-data` import. Callers supply the dataset so this util can + * be reused by the React `` component (Next.js bundle) and by the + * build-time markdown-schema handler (tsx) without each having to navigate + * `shared-data`'s ESM/CJS interop independently. + */ +export function resolveSharedDataPath(dataset: unknown, path: string): string | number | undefined { + const selected = at(dataset as any, [path])[0] + if (typeof selected === 'object' && selected !== null) { + return `${(selected as any).value ?? ''} ${(selected as any).unit ?? ''}`.trim() + } + return selected +} diff --git a/apps/docs/content/_partials/log_connections_default_effective_date.mdx b/apps/docs/content/_partials/log_connections_default_effective_date.mdx new file mode 100644 index 00000000000..67a85fc52c1 --- /dev/null +++ b/apps/docs/content/_partials/log_connections_default_effective_date.mdx @@ -0,0 +1,5 @@ + + +This default takes effect for new projects from July 9, 2026. + + diff --git a/apps/docs/content/errorCodes/authErrorCodes.toml b/apps/docs/content/errorCodes/authErrorCodes.toml deleted file mode 100644 index 12505f7a8ac..00000000000 --- a/apps/docs/content/errorCodes/authErrorCodes.toml +++ /dev/null @@ -1,283 +0,0 @@ -# Official error codes for Supabase Auth -# -# Error codes should be documented in the following format -# -# [error_code] -# description = "Error description." -# resolution = "How to resolve this error." -# [[error_code.references]] -# href = "https://supabase.com/docs/some/relevant/guide" -# description = "Guide for doing some relevant thing" -# -# error_code should be a unique and stable identifier for the error, that the -# developer can match against for error handling. - -[anonymous_provider_disabled] -description = "Anonymous sign-ins are disabled." - -[bad_code_verifier] -description = "Returned from the PKCE flow where the provided code verifier does not match the expected one. Indicates a bug in the implementation of the client library." - -[bad_json] -description = "Usually used when the HTTP body of the request is not valid JSON." - -[bad_jwt] -description = "JWT sent in the Authorization header is not valid." - -[bad_oauth_callback] -description = "OAuth callback from provider to Auth does not have all the required attributes (state). Indicates an issue with the OAuth provider or client library implementation." - -[bad_oauth_state] -description = "OAuth state (data echoed back by the OAuth provider to Supabase Auth) is not in the correct format. Indicates an issue with the OAuth provider integration." - -[captcha_failed] -description = "CAPTCHA challenge could not be verified with the CAPTCHA provider. Check your CAPTCHA integration." - -[conflict] -description = "General database conflict, such as concurrent requests on resources that should not be modified concurrently. Can often occur when you have too many session refresh requests firing off at the same time for a user. Check your app for concurrency issues, and if detected, back off exponentially." - -[email_address_invalid] -description = "Example and test domains are currently not supported. Use a different email address." - -[email_address_not_authorized] -description = "Email sending is not allowed for this address as your project is using the default SMTP service. Emails can only be sent to members in your Supabase organization. If you want to send emails to others, set up a custom SMTP provider." -[[email_address_not_authorized.references]] -href = "https://supabase.com/docs/guides/auth/auth-smtp" -description = "Setting up a custom SMTP provider" - -[email_conflict_identity_not_deletable] -description = "Unlinking this identity causes the user's account to change to an email address which is already used by another user account. Indicates an issue where the user has two different accounts using different primary email addresses. You may need to migrate user data to one of their accounts in this case." - -[email_exists] -description = "Email address already exists in the system." - -[email_not_confirmed] -description = "Signing in is not allowed for this user as the email address is not confirmed." - -[email_provider_disabled] -description = "Signups are disabled for email and password." - -[flow_state_expired] -description = "PKCE flow state to which the API request relates has expired. Ask the user to sign in again." - -[flow_state_not_found] -description = "PKCE flow state to which the API request relates no longer exists. Flow states expire after a while and are progressively cleaned up, which can cause this error. Retried requests can cause this error, as the previous request likely destroyed the flow state. Ask the user to sign in again." - -[hook_payload_invalid_content_type] -description = "Payload from Auth does not have a valid Content-Type header." - -[hook_payload_over_size_limit] -description = "Payload from Auth exceeds maximum size limit." - -[hook_timeout] -description = "Unable to reach hook within maximum time allocated." - -[hook_timeout_after_retry] -description = "Unable to reach hook after maximum number of retries." - -[identity_already_exists] -description = "The identity to which the API relates is already linked to a user." - -[identity_not_found] -description = "Identity to which the API call relates does not exist, such as when an identity is unlinked or deleted." - -[insufficient_aal] -description = "To call this API, the user must have a higher Authenticator Assurance Level. To resolve, ask the user to solve an MFA challenge." -[[insufficient_aal.references]] -href = "https://supabase.com/docs/guides/auth/auth-mfa" -description = "MFA" - -[invite_not_found] -description = "Invite is expired or already used." - -[invalid_credentials] -description = "Login credentials or grant type not recognized." - -[manual_linking_disabled] -description = "Calling the supabase.auth.linkUser() and related APIs is not enabled on the Auth server." - -[mfa_challenge_expired] -description = "Responding to an MFA challenge should happen within a fixed time period. Request a new challenge when encountering this error." - -[mfa_factor_name_conflict] -description = "MFA factors for a single user should not have the same friendly name." - -[mfa_factor_not_found] -description = "MFA factor no longer exists." - -[mfa_ip_address_mismatch] -description = "The enrollment process for MFA factors must begin and end with the same IP address." - -[mfa_phone_enroll_not_enabled] -description = "Enrollment of MFA Phone factors is disabled." - -[mfa_phone_verify_not_enabled] -description = "Login via Phone factors and verification of new Phone factors is disabled." - -[mfa_totp_enroll_not_enabled] -description = "Enrollment of MFA TOTP factors is disabled." - -[mfa_totp_verify_not_enabled] -description = "Login via TOTP factors and verification of new TOTP factors is disabled." - -[mfa_verification_failed] -description = "MFA challenge could not be verified -- wrong TOTP code." - -[mfa_verification_rejected] -description = "Further MFA verification is rejected. Only returned if the MFA verification attempt hook returns a reject decision." -[[mfa_verification_rejected.references]] -href = "https://supabase.com/docs/guides/auth/auth-hooks/mfa-verification-hook" -description = "MFA verification hook" - -[mfa_verified_factor_exists] -description = "Verified phone factor already exists for a user. Unenroll existing verified phone factor to continue." - -[mfa_web_authn_enroll_not_enabled] -description = "Enrollment of MFA Web Authn factors is disabled." - -[mfa_web_authn_verify_not_enabled] -description = "Login via WebAuthn factors and verification of new WebAuthn factors is disabled." - -[no_authorization] -description = "This HTTP request requires an Authorization header, which is not provided." - -[not_admin] -description = "User accessing the API is not admin, i.e. the JWT does not contain a role claim that identifies them as an admin of the Auth server." - -[oauth_provider_not_supported] -description = "Using an OAuth provider which is disabled on the Auth server." - -[otp_disabled] -description = "Sign in with OTPs (magic link, email OTP) is disabled. Check your server's configuration." - -[otp_expired] -description = "OTP code for this sign-in has expired. Ask the user to sign in again." - -[over_email_send_rate_limit] -description = "Too many emails have been sent to this email address. Ask the user to wait a while before trying again." - -[over_request_rate_limit] -description = "Too many requests have been sent by this client (IP address). Ask the user to try again in a few minutes. Sometimes can indicate a bug in your application that mistakenly sends out too many requests (such as a badly written useEffect React hook)." -[[over_request_rate_limit.references]] -href = "https://react.dev/reference/react/useEffect" -description = "React useEffect hook" - -[over_sms_send_rate_limit] -description = "Too many SMS messages have been sent to this phone number. Ask the user to wait a while before trying again." - -[phone_exists] -description = "Phone number already exists in the system." - -[phone_not_confirmed] -description = "Signing in is not allowed for this user as the phone number is not confirmed." - -[phone_provider_disabled] -description = "Signups are disabled for phone and password." - -[provider_disabled] -description = "OAuth provider is disabled for use. Check your server's configuration." - -[provider_email_needs_verification] -description = "Not all OAuth providers verify their user's email address. Supabase Auth requires emails to be verified, so this error is sent out when a verification email is sent after completing the OAuth flow." - -[reauthentication_needed] -description = "A user needs to reauthenticate to change their password. Ask the user to reauthenticate by calling the supabase.auth.reauthenticate() API." - -[reauthentication_not_valid] -description = "Verifying a reauthentication failed, the code is incorrect. Ask the user to enter a new code." - -[refresh_token_not_found] -description = "Session containing the refresh token not found." - -[refresh_token_already_used] -description = "Refresh token has been revoked and falls outside the refresh token reuse interval. See the documentation on sessions for further information." -[[refresh_token_already_used.references]] -href = "https://supabase.com/docs/guides/auth/sessions" -description = "Auth sessions" - -[request_timeout] -description = "Processing the request took too long. Retry the request." - -[same_password] -description = "A user that is updating their password must use a different password than the one currently used." - -[saml_assertion_no_email] -description = "SAML assertion (user information) was received after sign in, but no email address was found in it, which is required. Check the provider's attribute mapping and/or configuration." - -[saml_assertion_no_user_id] -description = "SAML assertion (user information) was received after sign in, but a user ID (called NameID) was not found in it, which is required. Check the SAML identity provider's configuration." - -[saml_entity_id_mismatch] -description = "(Admin API.) Updating the SAML metadata for a SAML identity provider is not possible, as the entity ID in the update does not match the entity ID in the database. This is equivalent to creating a new identity provider, and you should do that instead." - -[saml_idp_already_exists] -description = "(Admin API.) Adding a SAML identity provider that is already added." - -[saml_idp_not_found] -description = "SAML identity provider not found. Most often returned after IdP-initiated sign-in with an unregistered SAML identity provider in Supabase Auth." - -[saml_metadata_fetch_failed] -description = "(Admin API.) Adding or updating a SAML provider failed as its metadata could not be fetched from the provided URL." - -[saml_provider_disabled] -description = "Using Enterprise SSO with SAML 2.0 is not enabled on the Auth server." -[[saml_provider_disabled.references]] -href = "https://supabase.com/docs/guides/auth/enterprise-sso/auth-sso-saml" -description = "Enterprise SSO" - -[saml_relay_state_expired] -description = "SAML relay state is an object that tracks the progress of a supabase.auth.signInWithSSO() request. The SAML identity provider should respond after a fixed amount of time, after which this error is shown. Ask the user to sign in again." - -[saml_relay_state_not_found] -description = "SAML relay states are progressively cleaned up after they expire, which can cause this error. Ask the user to sign in again." - -[session_expired] -description = "Session to which the API request relates has expired. This can occur if an inactivity timeout is configured, or the session entry has exceeded the configured timebox value. See the documentation on sessions for more information." -[[session_expired.references]] -href = "https://supabase.com/docs/guides/auth/sessions" -description = "Auth sessions" - -[session_not_found] -description = "Session to which the API request relates no longer exists. This can occur if the user has signed out, or the session entry in the database was deleted in some other way." - -[signup_disabled] -description = "Sign ups (new account creation) are disabled on the server." - -[single_identity_not_deletable] -description = "Every user must have at least one identity attached to it, so deleting (unlinking) an identity is not allowed if it's the only one for the user." - -[sms_send_failed] -description = "Sending an SMS message failed. Check your SMS provider configuration." - -[sso_domain_already_exists] -description = "(Admin API.) Only one SSO domain can be registered per SSO identity provider." - -[sso_provider_not_found] -description = "SSO provider not found. Check the arguments in supabase.auth.signInWithSSO()." - -[too_many_enrolled_mfa_factors] -description = "A user can only have a fixed number of enrolled MFA factors." - -[unexpected_audience] -description = "(Deprecated feature not available via Supabase client libraries.) The request's X-JWT-AUD claim does not match the JWT's audience." - -[unexpected_failure] -description = "Auth service is degraded or a bug is present, without a specific reason." - -[user_already_exists] -description = "User with this information (email address, phone number) cannot be created again as it already exists." - -[user_banned] -description = "User to which the API request relates has a banned_until property which is still active. No further API requests should be attempted until this field is cleared." - -[user_not_found] -description = "User to which the API request relates no longer exists." - -[user_sso_managed] -description = "When a user comes from SSO, certain fields of the user cannot be updated (like email)." - -[validation_failed] -description = "Provided parameters are not in the expected format." - -[weak_password] -description = "User is signing up or changing their password without meeting the password strength criteria. Use the AuthWeakPasswordError class to access more information about what they need to do to make the password pass." diff --git a/apps/docs/content/errorCodes/realtimeErrorCodes.toml b/apps/docs/content/errorCodes/realtimeErrorCodes.toml deleted file mode 100644 index 57b5f8f5e60..00000000000 --- a/apps/docs/content/errorCodes/realtimeErrorCodes.toml +++ /dev/null @@ -1,215 +0,0 @@ -# Official error codes for Supabase Realtime -# -# Error codes should be documented in the following format -# -# [error_code] -# description = "Error description." -# resolution = "How to resolve this error." -# [[error_code.references]] -# href = "https://supabase.com/docs/some/relevant/guide" -# description = "Guide for doing some relevant thing" -# -# error_code should be a unique and stable identifier for the error, that the -# developer can match against for error handling. - -[TopicNameRequired] -description = "You are trying to use Realtime without a topic name set." - -[RealtimeDisabledForConfiguration] -description = "The configuration provided to Realtime on connect will not be able to provide you any Postgres Changes." -resolution = "Verify your configuration on channel startup as you might not have your tables properly registered." - -[TenantNotFound] -description = "The tenant you are trying to connect to does not exist." -resolution = "Verify the tenant name you are trying to connect to exists in the realtime.tenants table." - -[ErrorConnectingToWebsocket] -description = "Error when trying to connect to the WebSocket server." -resolution = "Verify user information on connect." - -[ErrorAuthorizingWebsocket] -description = "Error when trying to authorize the WebSocket connection." -resolution = "Verify user information on connect." - -[TableHasSpacesInName] -description = "The table you are trying to listen to has spaces in its name which we are unable to support." -resolution = "Change the table name to not have spaces in it." - -[UnableToDeleteTenant] -description = "Error when trying to delete a tenant." - -[UnableToSetPolicies] -description = "Error when setting up Authorization Policies." - -[UnableCheckoutConnection] -description = "Error when trying to checkout a connection from the tenant pool." - -[UnableToSubscribeToPostgres] -description = "Error when trying to subscribe to Postgres changes." - -[ReconnectSubscribeToPostgres] -description = "Postgres changes still waiting to be subscribed." - -[ChannelRateLimitReached] -description = "The number of channels you can create has reached its limit." - -[ConnectionRateLimitReached] -description = "The number of connected clients has reached its limit." - -[ClientJoinRateLimitReached] -description = "The rate of joins per second from your clients has reached the channel limits." - -[RealtimeDisabledForTenant] -description = "Realtime has been disabled for the tenant." -resolution = "Your project may have been suspended for exceeding usage quotas. Contact support with your project reference ID and a description of your Realtime use case." -[[RealtimeDisabledForTenant.references]] -href = "https://supabase.com/docs/troubleshooting/realtime-project-suspended-for-exceeding-quotas" -description = "Troubleshooting guide for suspended projects" - -[UnableToConnectToTenantDatabase] -description = "Realtime was not able to connect to the tenant's database." - -[DatabaseLackOfConnections] -description = "Realtime was not able to connect to the tenant's database due to not having enough available connections." -resolution = "Verify your database connection limits." -[[DatabaseLackOfConnections.references]] -href = "https://supabase.com/docs/guides/database/connection-management" -description = "Connection management guide" - -[RealtimeNodeDisconnected] -description = "Realtime is a distributed application and this means that one the system is unable to communicate with one of the distributed nodes." - -[MigrationsFailedToRun] -description = "Error when running the migrations against the Tenant database that are required by Realtime." - -[StartListenAndReplicationFailed] -description = "Error when starting the replication and listening of errors for database broadcasting." - -[ReplicationMaxWalSendersReached] -description = "Maximum number of WAL senders reached in tenant database." -[[ReplicationMaxWalSendersReached.references]] -href = "https://supabase.com/docs/guides/database/custom-postgres-config#cli-configurable-settings" -description = "Configuring max WAL senders" - -[MigrationCheckFailed] -description = "Check to see if we require to run migrations fails." - -[PartitionCreationFailed] -description = "Error when creating partitions for realtime.messages." - -[ErrorStartingPostgresCDCStream] -description = "Error when starting the Postgres CDC stream which is used for Postgres Changes." - -[UnknownDataProcessed] -description = "An unknown data type was processed by the Realtime system." - -[ErrorStartingPostgresCDC] -description = "Error when starting the Postgres CDC extension which is used for Postgres Changes." - -[ReplicationSlotBeingUsed] -description = "The replication slot is being used by another transaction." - -[PoolingReplicationPreparationError] -description = "Error when preparing the replication slot." - -[PoolingReplicationError] -description = "Error when pooling the replication slot." - -[SubscriptionDeletionFailed] -description = "Error when trying to delete a subscription for postgres changes." - -[UnableToDeletePhantomSubscriptions] -description = "Error when trying to delete subscriptions that are no longer being used." - -[UnableToCheckProcessesOnRemoteNode] -description = "Error when trying to check the processes on a remote node." - -[UnableToCreateCounter] -description = "Error when trying to create a counter to track rate limits for a tenant." - -[UnableToIncrementCounter] -description = "Error when trying to increment a counter to track rate limits for a tenant." - -[UnableToDecrementCounter] -description = "Error when trying to decrement a counter to track rate limits for a tenant." - -[UnableToUpdateCounter] -description = "Error when trying to update a counter to track rate limits for a tenant." - -[UnableToFindCounter] -description = "Error when trying to find a counter to track rate limits for a tenant." - -[UnhandledProcessMessage] -description = "Unhandled message received by a Realtime process." - -[UnableToTrackPresence] -description = "Error when handling track presence for this socket." - -[UnknownPresenceEvent] -description = "Presence event type not recognized by service." - -[IncreaseConnectionPool] -description = "The number of connections you have set for Realtime are not enough to handle your current use case." - -[RlsPolicyError] -description = "Error on RLS policy used for authorization." - -[ConnectionInitializing] -description = "Database is initializing connection." - -[DatabaseConnectionIssue] -description = "Database had connection issues and connection was not able to be established." - -[UnableToConnectToProject] -description = "Unable to connect to Project database." - -[InvalidJWTExpiration] -description = "JWT exp claim value it's incorrect." - -[JwtSignatureError] -description = "JWT signature was not able to be validated." - -[MalformedJWT] -description = "Token received does not comply with the JWT format." - -[Unauthorized] -description = "Unauthorized access to Realtime channel." - -[RealtimeRestarting] -description = "Realtime is currently restarting." - -[UnableToProcessListenPayload] -description = "Payload sent in NOTIFY operation was not JSON parsable." - -[UnableToListenToTenantDatabase] -description = "Unable to LISTEN for notifications against the Tenant Database." - -[UnprocessableEntity] -description = "Received a HTTP request with a body that was not able to be processed by the endpoint." - -[InitializingProjectConnection] -description = "Connection against Tenant database is still starting." - -[TimeoutOnRpcCall] -description = "RPC request within the Realtime server has timed out." - -[ErrorOnRpcCall] -description = "Error when calling another realtime node." - -[ErrorExecutingTransaction] -description = "Error executing a database transaction in tenant database." - -[SynInitializationError] -description = "Our framework to syncronize processes has failed to properly startup a connection to the database." - -[JanitorFailedToDeleteOldMessages] -description = "Scheduled task for realtime.message cleanup was unable to run." - -[UnableToEncodeJson] -description = "An error were we are not handling correctly the response to be sent to the end user." - -[UnknownErrorOnController] -description = "An error we are not handling correctly was triggered on a controller." - -[UnknownErrorOnChannel] -description = "An error we are not handling correctly was triggered on a channel." diff --git a/apps/docs/content/guides/auth/auth-mfa/phone.mdx b/apps/docs/content/guides/auth/auth-mfa/phone.mdx index c903dd95165..649e87d4b97 100644 --- a/apps/docs/content/guides/auth/auth-mfa/phone.mdx +++ b/apps/docs/content/guides/auth/auth-mfa/phone.mdx @@ -12,16 +12,25 @@ The phone messaging configuration for MFA is shared with [phone auth login](/doc Below is a flow chart illustrating how the Enrollment and Verify APIs work in the context of MFA (Phone). - +```mermaid +flowchart TD + InitS((Setup flow)) --> SAAL1[/Session is AAL1/] + SAAL1 --> Enroll[Enroll API] + Enroll --> ChallengeAPI[Challenge API] + ChallengeAPI --> Scan[/Code sent to User/] + Scan --> Enter[User: Enter code] + Enter --> Verify[Verify API] + Verify --> Check{{Is code correct?}} + Check -->|Yes| AAL2[/Upgrade to AAL2/] + AAL2 --> Done((Done)) + Check -->|No| Enter + InitA((Login flow)) --> SignIn([User: Sign-in]) + SignIn --> AAL1[/Upgrade to AAL1/] + AAL1 --> ListFactors[List Factors API] + ListFactors -->|1 or more factors| OpenAuth([User: Select phone factor]) + OpenAuth --> Enter + ListFactors -->|0 factors| Setup[[Setup flow]] +``` ### Add enrollment flow diff --git a/apps/docs/content/guides/auth/auth-mfa/totp.mdx b/apps/docs/content/guides/auth/auth-mfa/totp.mdx index c1869fbb097..906c0b81b14 100644 --- a/apps/docs/content/guides/auth/auth-mfa/totp.mdx +++ b/apps/docs/content/guides/auth/auth-mfa/totp.mdx @@ -12,16 +12,25 @@ The use of a QR code was [initially introduced by Google Authenticator](https:// Below is a flow chart illustrating how the Enrollment, Challenge, and Verify APIs work in the context of MFA (TOTP). - +```mermaid +flowchart TD + InitS((Setup flow)) --> SAAL1[/Session is AAL1/] + SAAL1 --> Enroll[Enroll API] + Enroll --> ShowQR[Show QR code] + ShowQR --> Scan([User: Scan QR code in authenticator]) + Scan --> Enter([User: Enter code]) + Enter --> Verify[Challenge + Verify API] + Verify --> Check{{Is code correct?}} + Check -->|Yes| AAL2[/Upgrade to AAL2/] + AAL2 --> Done((Done)) + Check -->|No| Enter + InitA((Login flow)) --> SignIn([User: Sign-in]) + SignIn --> AAL1[/Upgrade to AAL1/] + AAL1 --> ListFactors[List Factors API] + ListFactors -->|1 or more factors| OpenAuth([User: Open authenticator]) + OpenAuth --> Enter + ListFactors -->|0 factors| Setup[[Setup flow]] +``` [TOTP MFA API](/docs/reference/javascript/auth-mfa-api) is free to use and is enabled on all Supabase projects by default. diff --git a/apps/docs/content/guides/auth/signing-keys.mdx b/apps/docs/content/guides/auth/signing-keys.mdx index 2fcd8b3ce59..771cc541fcc 100644 --- a/apps/docs/content/guides/auth/signing-keys.mdx +++ b/apps/docs/content/guides/auth/signing-keys.mdx @@ -74,31 +74,12 @@ Key rotation and revocation are one of the most important processes for maintain ### Lifetime of a signing key -
- - - -
- A newly created key starts off as standby, before being rotated into in use (becoming the current key) while the existing current key becomes previously used. At any point you can move a key from the previously used or revoked states back to being a standby key, and rotate to it. This gives you the confidence to revert back to an older key if you identify problems with the rotation, such as forgetting to update a component of your application that is relying on a specific key (for example, the legacy JWT secret). Each action on a key is reversible (except permanent deletion). -
- -
- | Action | Accepted JWT signatures | Description | | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Create a new key | Current key only, new key has not created any JWTs yet. | When you initially create a key, after choosing the signing algorithm or importing a private key you already have, it starts out in the standby state. If using an asymmetric key (RSA, Elliptic Curve) its public key will be available in the discovery endpoint. Supabase Auth does not use this key to create new JWTs. | diff --git a/apps/docs/content/guides/cron/quickstart.mdx b/apps/docs/content/guides/cron/quickstart.mdx index 9ec8b35a846..38cb3cd7cd6 100644 --- a/apps/docs/content/guides/cron/quickstart.mdx +++ b/apps/docs/content/guides/cron/quickstart.mdx @@ -46,8 +46,7 @@ select cron.schedule('permanent-cron-job-name', '30 seconds', 'CALL do_something size="medium" className="text-foreground-light mt-8 mb-6" > -
- @@ -68,7 +67,6 @@ select cron.schedule('permanent-cron-job-name', '30 seconds', 'CALL do_something -
diff --git a/apps/docs/content/guides/database/connecting-to-postgres.mdx b/apps/docs/content/guides/database/connecting-to-postgres.mdx index 20e48681bd6..eace71d0811 100644 --- a/apps/docs/content/guides/database/connecting-to-postgres.mdx +++ b/apps/docs/content/guides/database/connecting-to-postgres.mdx @@ -315,14 +315,16 @@ Because the dedicated pooler is hosted on the same machine as your database, it See the [connection method matrix](#how-to-connect-to-your-postgres-databases) at the top of this page for a quick reference, or follow the decision flow in the diagram below to choose the right option for your environment. - +```mermaid +flowchart TD + A[Where are you connecting from?] --> B[Persistent Backend] + A --> C[Serverless / Edge] + B --> D{IPv6 Supported?
IPv4 Add-on?} + B --> E{IPv4 Needed?} + C --> H{IPv6 Supported?
IPv4 Add-on?} + C --> I{IPv4 Needed?} + D --> F[Use Direct Connection] + E --> G[Use Supavisor Session Mode] + H --> J[Use Dedicated Pooler PgBouncer Pro] + I --> K[Use Supavisor Transaction Mode] +``` diff --git a/apps/docs/content/guides/database/custom-postgres-config.mdx b/apps/docs/content/guides/database/custom-postgres-config.mdx index 51c533bf808..5e382e5cf11 100644 --- a/apps/docs/content/guides/database/custom-postgres-config.mdx +++ b/apps/docs/content/guides/database/custom-postgres-config.mdx @@ -149,6 +149,10 @@ Use the examples below with `supabase --experimental --project-ref | [wal_sender_timeout](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT) | CLI only | No | `--config wal_sender_timeout=60s` | | [work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM) | CLI + SQL | No | `--config work_mem=64MB` | +#### Management API only parameters + +Some Postgres settings are configurable through the [Management API](/docs/reference/api/v1-update-postgres-config) but not the CLI. These include logging settings such as `log_connections`. See [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) for details. + #### Managing Postgres configuration with the CLI To start: @@ -160,25 +164,25 @@ To update Postgres configurations, use the [`postgres config`](/docs/reference/c ```bash supabase --experimental \ ---project-ref \ -postgres-config update --config shared_buffers=250MB +postgres-config update --config shared_buffers=250MB \ +--project-ref ``` By default, the CLI will merge any provided config overrides with any existing ones. The `--replace-existing-overrides` flag can be used to instead force all existing overrides to be replaced with the ones being provided: ```bash supabase --experimental \ ---project-ref \ postgres-config update --config max_parallel_workers=3 \ ---replace-existing-overrides +--replace-existing-overrides \ +--project-ref ``` To delete specific configuration overrides, use the `postgres-config delete` command: ```bash supabase --experimental \ ---project-ref \ -postgres-config delete --config shared_buffers,work_mem +postgres-config delete --config shared_buffers,work_mem \ +--project-ref ``` By default, CLI v2 (≥ 2.0.0) checks the parameter’s context and requests the correct action (reload or restart): diff --git a/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx b/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx index ff3757e0eab..4918a182eb2 100644 --- a/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx +++ b/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx @@ -25,15 +25,15 @@ These options, called "query parameters," can be used to address specific errors connection_string.../postgres?KEY1=VALUE&KEY2=VALUE&KEY3=VALUE ``` -# Errors +## Errors {/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} -## ... prepared statement already exists +### Prepared statement already exists Supavisor in transaction mode (port 6543) does not support [prepared statements](https://www.postgresql.org/docs/current/sql-prepare.html), which Prisma will try to create in the background. -### Solution: [#solution-prepared-statement-exists] +#### Solution: [#solution-prepared-statement-exists] - Add `pgbouncer=true` to the connection string. This turns off prepared statements in Prisma. @@ -43,17 +43,17 @@ Supavisor in transaction mode (port 6543) does not support [prepared statements] --- -## Can't reach database server at: +### Can't reach the database server -Prisma couldn't establish a connection with Postgres or Supavisor before the timeout +Prisma couldn't establish a connection with Postgres or Supavisor before the timeout. -### Possible causes: [#possible-causes-cant-reach-database-server-at] +#### Possible causes: [#possible-causes-cant-reach-database-server-at] - **Database overload**: The database server is under heavy load, causing Prisma to struggle to connect. - **Malformed connection string**: The connection string used by Prisma is incorrect or incomplete. - **Transient network issues**: Temporary network problems are disrupting the connection. -### Solutions: [#solution-cant-reach-database-server-at] +#### Solutions: [#solution-cant-reach-database-server-at] - **Check database health**: Use the [Observability Dashboard](/dashboard/project/_/observability/database) to monitor CPU, memory, and I/O usage. If the database is overloaded, consider increasing your [compute size](/docs/guides/platform/compute-add-ons) or [optimizing your queries](/docs/guides/database/query-optimization). - **Verify connection string**: Double-check the connection string in your Prisma configuration to ensure it matches in your [project connect page](/dashboard/project/_?showConnect=true). @@ -65,17 +65,17 @@ Prisma couldn't establish a connection with Postgres or Supavisor before the tim --- -## Timed out fetching a new connection from the connection pool: +### Timed out fetching a new connection from the connection pool Prisma is unable to allocate connections to pending queries fast enough to meet demand. -### Possible causes: [#possible-causes-timed-out-fetching-a-new-connection] +#### Possible causes: [#possible-causes-timed-out-fetching-a-new-connection] - **Overwhelmed server**: The server hosting Prisma is under heavy load, limiting its ability to manage connections. By default, Prisma will create the default `num_cpus * 2 + 1` worth of connections. A common cause for server strain is increasing the `connection_limit` significantly past the default. - **Insufficient pool size**: The Supavisor pooler does not have enough connections available to quickly satisfy Prisma's requests. - **Slow queries**: Prisma's queries are taking too long to execute, preventing it from releasing connections for reuse. -### Solutions: [#solution-timed-out-fetching-a-new-connection] +#### Solutions: [#solution-timed-out-fetching-a-new-connection] - **Increase the pool timeout**: Increase the `pool_timeout` parameter in your Prisma configuration to give the pooler more time to allocate connections. - **Reduce the connection limit**: If you've explicitly increased the `connection_limit` parameter in your Prisma configuration, try reducing it to a more reasonable value. @@ -85,43 +85,43 @@ Prisma is unable to allocate connections to pending queries fast enough to meet --- -## Server has closed the connection +### Server has closed the connection According to this [GitHub Issue for Prisma](https://github.com/prisma/prisma/discussions/7389), this error may be related to large return values for queries. It may also be caused by significant database strain. -### Solutions: [#solution-server-has-closed-the-connection] +#### Solutions: [#solution-server-has-closed-the-connection] - **Limit row return sizes**: Try to limit the total amount of rows returned for particularly large requests. - **Minimize database strain**:Check the Reports Page for database strain. If there is obvious strain, consider [optimizing](/docs/guides/database/query-optimization) or increasing compute size --- -## Drift detected: Your database schema is not in sync with your migration history +### Drift detected: Your database schema is not in sync with your migration history Prisma relies on migration files to ensure your database aligns with Prisma's model. External schema changes are detected as "drift", which Prisma will try to overwrite, potentially causing data loss. -### Possible causes: [#possible-causes-your-database-schema-is-not-in-sync] +#### Possible causes: [#possible-causes-your-database-schema-is-not-in-sync] - **Supabase Managed Schemas**: Supabase may update managed schemas like auth and storage to introduce new features. Granting Prisma access to these schemas can lead to drift during updates. - **External Schema Modifications**: Your team or another tool might have modified the database schema outside of Prisma, causing drift. -### Solution: [#solution-your-database-schema-is-not-in-sync] +#### Solution: [#solution-your-database-schema-is-not-in-sync] - **Baselining migrations**: [baselining](https://www.prisma.io/docs/orm/prisma-migrate/workflows/baselining) re-syncs Prisma by capturing the current database schema as the starting point for future migrations. --- -## Max client connections reached +### Max client connections reached Postgres or Supavisor rejected a request for more connections -### Possible causes:[#possible-causes-max-client-connections-reached] +#### Possible causes:[#possible-causes-max-client-connections-reached] - **When working in transaction mode (port 6543):** The error "Max client connections reached" occurs when clients try to form more connections with the pooler than it can support. - **When working in session mode (port 5432):** The max amount of clients is restricted to the "Pool Size" value in the [Database Settings](/dashboard/project/_/database/settings). If the "Pool Size" is set to 15, even if the pooler can handle 200 client connections, it will still be effectively capped at 15 for each unique ["database-role+database" combination](https://github.com/orgs/supabase/discussions/21566). - **When working with direct connections**: Postgres is already servicing the max amount of connections -### Solutions [#solutions-causes-max-client-connections-reached] +#### Solutions [#solutions-causes-max-client-connections-reached] - **Transaction Mode for serverless apps**: If you are using serverless functions (Supabase Edge, Vercel, AWS Lambda), switch to transaction mode (port 6543). It handles more connections than session mode or direct connections. - **Reduce the number of Prisma connections**: A single client-server can establish multiple connections with a pooler. Typically, serverless setups do not need many connections. Starting with fewer, like five or three, or even just one, is often sufficient. In serverless setups, begin with `connection_limit=1`, increasing cautiously if needed to avoid maxing out connections. @@ -132,15 +132,15 @@ Postgres or Supavisor rejected a request for more connections --- -## Cross schema references are only allowed when the target schema is listed in the schemas property of your data-source +### Cross schema references are only allowed when the target schema is listed in the schemas property of your data-source A Prisma migration is referencing a schema it is not permitted to manage. -### Possible causes: [#possible-causes-cross-schema-references] +#### Possible causes: [#possible-causes-cross-schema-references] - A migration references a schema that Prisma is not permitted to manage -### Solutions: [#solutions-cross-schema-references] +#### Solutions: [#solutions-cross-schema-references] - Multi-schema support: If the external schema isn't Supabase managed, list the relevant schemas on the `datasource` block in your `schema.prisma` file. diff --git a/apps/docs/content/guides/database/testing.mdx b/apps/docs/content/guides/database/testing.mdx index a4e93b38eeb..17c9a5abd3e 100644 --- a/apps/docs/content/guides/database/testing.mdx +++ b/apps/docs/content/guides/database/testing.mdx @@ -10,13 +10,13 @@ To ensure that queries return the expected data, RLS policies are correctly appl - Secondly, you can test through the Supabase CLI, which is a more low-level approach where you write tests in SQL. -# Testing using the Supabase CLI +## Testing using the Supabase CLI You can use the Supabase CLI to test your database. The minimum required version of the CLI is [v1.11.4](https://github.com/supabase/cli/releases). To get started: - [Install the Supabase CLI](/docs/guides/cli) on your local machine -## Creating a test +### Creating a test Create a tests folder inside the `supabase` folder: @@ -30,7 +30,7 @@ Create a new file with the `.sql` extension which will contain the test. touch ./supabase/tests/database/hello_world.test.sql ``` -## Writing tests +### Writing tests All `sql` files use [pgTAP](/docs/guides/database/extensions/pgtap) as the test runner. @@ -51,7 +51,7 @@ select * from finish(); rollback; ``` -## Running tests +### Running tests To run the test, you can use: @@ -69,7 +69,7 @@ Files=1, Tests=1, 1 wallclock secs ( 0.01 usr 0.00 sys + 0.04 cusr 0.02 csys Result: PASS ``` -## More resources +### More resources - [Testing RLS policies](/docs/guides/database/extensions/pgtap#testing-rls-policies) - [pgTAP extension](/docs/guides/database/extensions/pgtap) diff --git a/apps/docs/content/guides/deployment/shared-responsibility-model.mdx b/apps/docs/content/guides/deployment/shared-responsibility-model.mdx index 41c934fd008..5cfed3746e3 100644 --- a/apps/docs/content/guides/deployment/shared-responsibility-model.mdx +++ b/apps/docs/content/guides/deployment/shared-responsibility-model.mdx @@ -98,6 +98,10 @@ You can use Supabase to store and process Protected Health Information (PHI). Yo - Enabling [Point in Time Recovery](/docs/guides/platform/backups#point-in-time-recovery) which requires at least a [small compute add-on](/docs/guides/platform/compute-add-ons). - Turning on [SSL Enforcement](/docs/guides/platform/ssl-enforcement). - Enabling [Network Restrictions](/docs/guides/platform/network-restrictions). +- Keeping [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) enabled. Supabase sets `log_connections` to off by default for new projects. Projects that need HIPAA compliance should keep connection logging on for audit trails, and the Security Advisor warns if it is disabled. + +<$Partial path="log_connections_default_effective_date.mdx" /> + - Complying with encryption requirements in the HIPAA Security Rule. Data is encrypted at rest and in transit by Supabase. You can consider encrypting the data at your application layer. - Not storing PHI in [public Storage buckets](/docs/guides/storage/buckets/fundamentals#public-buckets). - Not [transferring projects](/docs/guides/platform/project-transfer) to a non-HIPAA organization. diff --git a/apps/docs/content/guides/functions/ai-models.mdx b/apps/docs/content/guides/functions/ai-models.mdx index b349cfe424f..816c715bfe6 100644 --- a/apps/docs/content/guides/functions/ai-models.mdx +++ b/apps/docs/content/guides/functions/ai-models.mdx @@ -358,7 +358,7 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it ```ts import { withSupabase } from 'npm:@supabase/server@^1' - import OpenAI from 'https://deno.land/x/openai@v4.53.2/mod.ts' + import OpenAI from 'jsr:@openai/openai@^6' export default { fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => { diff --git a/apps/docs/content/guides/functions/background-tasks.mdx b/apps/docs/content/guides/functions/background-tasks.mdx index adde605ed48..c893c8227b6 100644 --- a/apps/docs/content/guides/functions/background-tasks.mdx +++ b/apps/docs/content/guides/functions/background-tasks.mdx @@ -27,7 +27,7 @@ EdgeRuntime.waitUntil(asyncLongRunningTask()) export default { fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { - return new Response(...) + return Response.json({ ok: true }) }), } ``` @@ -42,7 +42,7 @@ export default { // Won't block the request, runs in background. EdgeRuntime.waitUntil(asyncLongRunningTask()) - return new Response(...) + return Response.json({ ok: true }) }), } ``` @@ -62,7 +62,7 @@ addEventListener('beforeunload', (ev) => { export default { fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { - return new Response(...) + return Response.json({ ok: true }) }), } ``` diff --git a/apps/docs/content/guides/functions/connect-to-postgres.mdx b/apps/docs/content/guides/functions/connect-to-postgres.mdx index b3df92c5411..34869b62e5a 100644 --- a/apps/docs/content/guides/functions/connect-to-postgres.mdx +++ b/apps/docs/content/guides/functions/connect-to-postgres.mdx @@ -31,7 +31,7 @@ export default { return Response.json({ data }) } catch (err) { - return new Response(String(err?.message ?? err), { status: 500 }) + return Response.json({ error: String(err?.message ?? err) }, { status: 500 }) } }), } diff --git a/apps/docs/content/guides/functions/cors.mdx b/apps/docs/content/guides/functions/cors.mdx index 420aa5a74aa..eb0b2a1ed2f 100644 --- a/apps/docs/content/guides/functions/cors.mdx +++ b/apps/docs/content/guides/functions/cors.mdx @@ -31,10 +31,10 @@ If your function doesn't use `withSupabase`, add the headers yourself. See the [
-Import `corsHeaders` from `@supabase/supabase-js/cors` to automatically get all required headers: +Import `corsHeaders` from `npm:@supabase/supabase-js@^2/cors` to automatically get all required headers: ```ts index.ts -import { corsHeaders } from '@supabase/supabase-js/cors' +import { corsHeaders } from 'npm:@supabase/supabase-js@^2/cors' console.log(`Function "browser-with-cors" up and running!`) @@ -42,7 +42,7 @@ export default { fetch: async (req) => { // Handle the CORS preflight request. if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }) + return Response.json({ ok: true }, { headers: corsHeaders }) } try { diff --git a/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx b/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx index 04c258cb300..3ab5e0ab092 100644 --- a/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx +++ b/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx @@ -46,9 +46,9 @@ And add the code to the `index.ts` file: ```ts index.ts // We need to mock the file system for the AWS SDK to work. import { prepareVirtualFile } from 'https://deno.land/x/mock_file@v1.1.2/mod.ts' -import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime' +import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime@^3' import { withSupabase } from 'npm:@supabase/server@^1' -import { decode } from 'npm:base64-arraybuffer' +import { decode } from 'npm:base64-arraybuffer@^1' console.log('Hello from Amazon Bedrock!') @@ -108,7 +108,7 @@ export default { upsert: false, }) if (!upload) { - return Response.json(uploadError) + return Response.json({ error: uploadError?.message ?? 'Upload failed' }, { status: 500 }) } const { data } = ctx.supabase.storage.from('images').getPublicUrl(upload.path!) return Response.json(data) diff --git a/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx b/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx index 7b10c3140c6..459f7b7ae87 100644 --- a/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx +++ b/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx @@ -34,11 +34,11 @@ supabase functions new send-email Paste the following code into the `index.ts` file: ```tsx supabase/functions/send-email/index.ts -import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0' -import { renderAsync } from 'npm:@react-email/components@0.0.22' +import { Webhook } from 'npm:standardwebhooks@^1' +import { renderAsync } from 'npm:@react-email/components@^1' import { withSupabase } from 'npm:@supabase/server@^1' -import React from 'npm:react@18.3.1' -import { Resend } from 'npm:resend@4.0.0' +import React from 'npm:react@^19' +import { Resend } from 'npm:resend@^6' import { MagicLinkEmail } from './_templates/magic-link.tsx' @@ -48,7 +48,7 @@ const hookSecret = (Deno.env.get('SEND_EMAIL_HOOK_SECRET') as string).replace('v export default { fetch: withSupabase({ auth: 'none' }, async (req) => { if (req.method !== 'POST') { - return new Response('not allowed', { status: 400 }) + return Response.json({ error: 'not allowed' }, { status: 400 }) } const payload = await req.text() @@ -124,8 +124,8 @@ import { Link, Preview, Text, -} from 'npm:@react-email/components@0.0.22' -import * as React from 'npm:react@18.3.1' +} from 'npm:@react-email/components@^1' +import * as React from 'npm:react@^19' interface MagicLinkEmailProps { supabase_url: string diff --git a/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx b/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx index f28412b1025..c845dc25ffe 100644 --- a/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx +++ b/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx @@ -54,9 +54,9 @@ export default { const outcome = await result.json() console.log(outcome) if (outcome.success) { - return new Response('success') + return Response.json({ success: true }) } - return new Response('failure') + return Response.json({ success: false }) }), } ``` diff --git a/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx b/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx index d6ef95ec477..c013b25d8fb 100644 --- a/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx +++ b/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx @@ -102,8 +102,8 @@ In your newly created `supabase/functions/text-to-speech/index.ts` file, add the import 'jsr:@supabase/functions-js/edge-runtime.d.ts' import { withSupabase } from 'npm:@supabase/server@^1' -import { ElevenLabsClient } from 'npm:elevenlabs@1.52.0' -import * as hash from 'npm:object-hash' +import { ElevenLabsClient } from 'npm:elevenlabs@^1' +import * as hash from 'npm:object-hash@^3' const client = new ElevenLabsClient({ apiKey: Deno.env.get('ELEVENLABS_API_KEY'), diff --git a/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx b/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx index e4913a30afa..8ec31003e50 100644 --- a/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx +++ b/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx @@ -107,13 +107,13 @@ Since Supabase Edge Function uses the [Deno runtime](https://deno.land/), you do In your newly created `scribe-bot/index.ts` file, add the following code: ```ts supabase/functions/scribe-bot/index.ts -import { Bot, webhookCallback } from 'https://deno.land/x/grammy@v1.34.0/mod.ts' +import { Bot, webhookCallback } from 'npm:grammy@^1' import 'jsr:@supabase/functions-js/edge-runtime.d.ts' import { withSupabase } from 'npm:@supabase/server@^1' -import type { SupabaseClient } from 'npm:@supabase/supabase-js@2' -import { ElevenLabsClient } from 'npm:elevenlabs@1.50.5' +import type { SupabaseClient } from 'npm:@supabase/supabase-js@^2' +import { ElevenLabsClient } from 'npm:elevenlabs@^1' console.log(`Function "elevenlabs-scribe-bot" up and running!`) @@ -235,7 +235,7 @@ export default { try { const url = new URL(req.url) if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) { - return new Response('not allowed', { status: 405 }) + return Response.json({ error: 'not allowed' }, { status: 405 }) } supabaseAdmin = ctx.supabaseAdmin diff --git a/apps/docs/content/guides/functions/examples/og-image.mdx b/apps/docs/content/guides/functions/examples/og-image.mdx index 71cef4ad1ee..4a19c24f374 100644 --- a/apps/docs/content/guides/functions/examples/og-image.mdx +++ b/apps/docs/content/guides/functions/examples/og-image.mdx @@ -21,8 +21,8 @@ Generate Open Graph images with Deno and Supabase Edge Functions. [View on GitHu Create a `handler.tsx` file to construct the OG image in React: ```tsx handler.tsx -import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts' -import React from 'https://esm.sh/react@18.2.0' +import { ImageResponse } from 'npm:@vercel/og@^0' +import React from 'npm:react@^19' export default function handler(req: Request) { return new ImageResponse( diff --git a/apps/docs/content/guides/functions/examples/push-notifications.mdx b/apps/docs/content/guides/functions/examples/push-notifications.mdx index dc9b26e33d3..f6a80ac2aea 100644 --- a/apps/docs/content/guides/functions/examples/push-notifications.mdx +++ b/apps/docs/content/guides/functions/examples/push-notifications.mdx @@ -164,7 +164,7 @@ Push notifications are an important part of any mobile app. They allow you to se ```ts supabase/functions/push/index.ts import { withSupabase } from 'npm:@supabase/server@^1' - import { JWT } from 'npm:google-auth-library@9' + import { JWT } from 'npm:google-auth-library@^10' import serviceAccount from '../service-account.json' with { type: 'json' } interface Notification { diff --git a/apps/docs/content/guides/functions/examples/semantic-search.mdx b/apps/docs/content/guides/functions/examples/semantic-search.mdx index 78d803f4b1d..b278fc12503 100644 --- a/apps/docs/content/guides/functions/examples/semantic-search.mdx +++ b/apps/docs/content/guides/functions/examples/semantic-search.mdx @@ -62,7 +62,7 @@ export default { .eq('id', id) if (error) console.warn(error.message) - return new Response('ok') + return Response.json({ ok: true }) }), } ``` @@ -112,7 +112,7 @@ const model = new Supabase.ai.Session('gte-small') export default { fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { const { search } = await req.json() - if (!search) return new Response('Please provide a search param!') + if (!search) return Response.json({ error: 'Please provide a search param!' }, { status: 400 }) // Generate embedding for search term. const embedding = await model.run(search, { mean_pool: true, @@ -128,7 +128,7 @@ export default { .select('content') .limit(3) if (error) { - return Response.json(error) + return Response.json({ error: error.message }, { status: 500 }) } return Response.json({ search, result }) diff --git a/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx b/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx index 9a691e28233..1bdb92f7b96 100644 --- a/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx +++ b/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx @@ -23,12 +23,12 @@ supabase functions new sentryfied Handle exceptions within your function and send them to Sentry. ```tsx -import * as Sentry from 'https://deno.land/x/sentry/index.mjs' +import * as Sentry from 'npm:@sentry/deno@^8' import { withSupabase } from 'npm:@supabase/server@^1' Sentry.init({ // https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/#where-to-find-your-dsn - dsn: SENTRY_DSN, + dsn: Deno.env.get('SENTRY_DSN'), defaultIntegrations: false, // Performance Monitoring tracesSampleRate: 1.0, @@ -55,7 +55,7 @@ export default { Sentry.captureException(e) // Flush Sentry before the running process closes await Sentry.flush(2000) - return Response.json({ msg: 'error' }, { status: 500 }) + return Response.json({ error: 'Internal Server Error' }, { status: 500 }) } }), } diff --git a/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx b/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx index 9f78d84aedd..1e8b854fdbe 100644 --- a/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx +++ b/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx @@ -27,7 +27,7 @@ set SLACK_TOKEN= Here's the code of the Edge Function, you can change the response to handle the text received: ```ts index.ts -import { WebClient } from 'https://deno.land/x/slack_web_api@6.7.2/mod.js' +import { WebClient } from 'npm:@slack/web-api@^7' import { withSupabase } from 'npm:@supabase/server@^1' const slackBotToken = Deno.env.get('SLACK_TOKEN') ?? '' @@ -55,7 +55,7 @@ export default { text: `Hello <@${user}>!`, thread_ts: ts, }) - return new Response('ok', { status: 200 }) + return Response.json({ ok: true }) } } catch (error) { return Response.json({ error: error.message }, { status: 500 }) diff --git a/apps/docs/content/guides/functions/examples/upstash-redis.mdx b/apps/docs/content/guides/functions/examples/upstash-redis.mdx index 630de49aca5..7cd80c8ddaf 100644 --- a/apps/docs/content/guides/functions/examples/upstash-redis.mdx +++ b/apps/docs/content/guides/functions/examples/upstash-redis.mdx @@ -39,7 +39,7 @@ supabase functions new upstash-redis-counter And add the code to the `index.ts` file: ```ts index.ts -import { Redis } from 'https://deno.land/x/upstash_redis@v1.19.3/mod.ts' +import { Redis } from 'npm:@upstash/redis@^1' import { withSupabase } from 'npm:@supabase/server@^1' console.log(`Function "upstash-redis-counter" up and running!`) diff --git a/apps/docs/content/guides/functions/kysely-postgres.mdx b/apps/docs/content/guides/functions/kysely-postgres.mdx index 0d61b084322..9b98eb9b869 100644 --- a/apps/docs/content/guides/functions/kysely-postgres.mdx +++ b/apps/docs/content/guides/functions/kysely-postgres.mdx @@ -34,7 +34,7 @@ GET YOUR CERT FROM YOUR PROJECT DASHBOARD Create a `DenoPostgresDriver.ts` file to manage the connection to Postgres via [deno-postgres](https://deno-postgres.com/): ```ts DenoPostgresDriver.ts -import { Pool, PoolClient } from 'https://deno.land/x/postgres@v0.17.0/mod.ts' +import { Pool, PoolClient } from 'jsr:@db/postgres@^0' import { CompiledQuery, DatabaseConnection, @@ -42,9 +42,9 @@ import { PostgresCursorConstructor, QueryResult, TransactionSettings, -} from 'https://esm.sh/kysely@0.23.4' -import { freeze, isFunction } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/object-utils.js' -import { extendStackTrace } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/stack-trace-utils.js' +} from 'npm:kysely@^0' +import { freeze, isFunction } from 'npm:kysely@^0/dist/esm/util/object-utils.js' +import { extendStackTrace } from 'npm:kysely@^0/dist/esm/util/stack-trace-utils.js' export interface PostgresDialectConfig { pool: Pool | (() => Promise) @@ -190,14 +190,14 @@ class PostgresConnection implements DatabaseConnection { Create an `index.ts` file to execute a query on incoming requests: ```ts index.ts -import { Pool } from 'https://deno.land/x/postgres@v0.17.0/mod.ts' +import { Pool } from 'jsr:@db/postgres@^0' import { Generated, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler, -} from 'https://esm.sh/kysely@0.23.4' +} from 'npm:kysely@^0' import { withSupabase } from 'npm:@supabase/server@^1' import { PostgresDriver } from './DenoPostgresDriver.ts' @@ -258,23 +258,23 @@ export default { // Neat, it's properly typed \o/ console.log(animals[0].created_at.getFullYear()) - // Encode the result as pretty printed JSON - const body = JSON.stringify( - animals, - (key, value) => (typeof value === 'bigint' ? value.toString() : value), - 2 + const data = animals.map((animal) => + Object.fromEntries( + Object.entries(animal).map(([key, value]) => [ + key, + typeof value === 'bigint' ? value.toString() : value, + ]) + ) ) - // Return the response with the correct content type header - return new Response(body, { - status: 200, + return Response.json(data, { headers: { 'Content-Type': 'application/json; charset=utf-8', }, }) } catch (err) { console.error(err) - return new Response(String(err?.message ?? err), { status: 500 }) + return Response.json({ error: String(err?.message ?? err) }, { status: 500 }) } }), } diff --git a/apps/docs/content/guides/functions/routing.mdx b/apps/docs/content/guides/functions/routing.mdx index b6c084fe134..872f5d708d6 100644 --- a/apps/docs/content/guides/functions/routing.mdx +++ b/apps/docs/content/guides/functions/routing.mdx @@ -44,13 +44,13 @@ import { withSupabase } from 'npm:@supabase/server@^1' export default { fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { if (req.method === 'GET') { - return new Response('Hello World!') + return Response.json({ message: 'Hello World!' }) } const { name } = await req.json() if (name) { - return new Response(`Hello ${name}!`) + return Response.json({ message: `Hello ${name}!` }) } - return new Response('Hello World!') + return Response.json({ message: 'Hello World!' }) }), } ``` @@ -60,7 +60,7 @@ export default { ```ts -import express from 'npm:express@4.18.2' +import express from 'npm:express@^5' const app = express() app.use(express.json()) @@ -70,12 +70,12 @@ app.use(express.json()) const port = 3000 app.get('/hello-world', (req, res) => { - res.send('Hello World!') + res.json({ message: 'Hello World!' }) }) app.post('/hello-world', (req, res) => { const { name } = req.body - res.send(`Hello ${name}!`) + res.json({ message: `Hello ${name}!` }) }) app.listen(port, () => { @@ -88,18 +88,18 @@ app.listen(port, () => { ```ts -import { Application } from 'jsr:@oak/oak@15/application' -import { Router } from 'jsr:@oak/oak@15/router' +import { Application } from 'jsr:@oak/oak@^17/application' +import { Router } from 'jsr:@oak/oak@^17/router' const router = new Router() router.get('/hello-world', (ctx) => { - ctx.response.body = 'Hello world!' + ctx.response.body = { message: 'Hello World!' } }) router.post('/hello-world', async (ctx) => { const { name } = await ctx.request.body.json() - ctx.response.body = `Hello ${name}!` + ctx.response.body = { message: `Hello ${name}!` } }) const app = new Application() @@ -114,17 +114,17 @@ app.listen({ port: 3000 }) ```ts -import { Hono } from 'jsr:@hono/hono' +import { Hono } from 'jsr:@hono/hono@^4' const app = new Hono() app.post('/hello-world', async (c) => { const { name } = await c.req.json() - return new Response(`Hello ${name}!`) + return c.json({ message: `Hello ${name}!` }) }) app.get('/hello-world', (c) => { - return new Response('Hello World!') + return c.json({ message: 'Hello World!' }) }) export default { fetch: app.fetch } @@ -173,15 +173,15 @@ let tasks: Task[] = [] const router = new Map Promise>() async function getAllTasks(): Promise { - return new Response(JSON.stringify(tasks)) + return Response.json({ tasks }) } async function getTask(id: string): Promise { const task = tasks.find((t) => t.id === id) if (task) { - return new Response(JSON.stringify(task)) + return Response.json({ task }) } else { - return new Response('Task not found', { status: 404 }) + return Response.json({ error: 'Task not found' }, { status: 404 }) } } @@ -189,16 +189,17 @@ async function createTask(req: Request): Promise { const id = Math.random().toString(36).substring(7) const task = { id, name: '' } tasks.push(task) - return new Response(JSON.stringify(task), { status: 201 }) + return Response.json({ task }, { status: 201 }) } async function updateTask(id: string, req: Request): Promise { const index = tasks.findIndex((t) => t.id === id) if (index !== -1) { - tasks[index] = { ...tasks[index] } - return new Response(JSON.stringify(tasks[index])) + const updates = await req.json() + tasks[index] = { ...tasks[index], ...updates } + return Response.json({ task: tasks[index] }) } else { - return new Response('Task not found', { status: 404 }) + return Response.json({ error: 'Task not found' }, { status: 404 }) } } @@ -206,9 +207,9 @@ async function deleteTask(id: string): Promise { const index = tasks.findIndex((t) => t.id === id) if (index !== -1) { tasks.splice(index, 1) - return new Response('Task deleted successfully') + return Response.json({ message: 'Task deleted successfully' }) } else { - return new Response('Task not found', { status: 404 }) + return Response.json({ error: 'Task not found' }, { status: 404 }) } } @@ -234,19 +235,19 @@ export default { if (id) { return updateTask(id, req) } else { - return new Response('Bad Request', { status: 400 }) + return Response.json({ error: 'Bad Request' }, { status: 400 }) } case 'DELETE': if (id) { return deleteTask(id) } else { - return new Response('Bad Request', { status: 400 }) + return Response.json({ error: 'Bad Request' }, { status: 400 }) } default: - return new Response('Method Not Allowed', { status: 405 }) + return Response.json({ error: 'Method Not Allowed' }, { status: 405 }) } } catch (error) { - return new Response(`Internal Server Error: ${error}`, { status: 500 }) + return Response.json({ error: `Internal Server Error: ${error}` }, { status: 500 }) } }), } @@ -257,7 +258,7 @@ export default { ```ts -import express from 'npm:express@4.18.2' +import express from 'npm:express@^5' const app = express() app.use(express.json()) @@ -293,8 +294,8 @@ app.delete('/tasks/:id', async (req, res) => { ```ts -import { Application } from 'jsr:@oak/oak/application' -import { Router } from 'jsr:@oak/oak/router' +import { Application } from 'jsr:@oak/oak@^17/application' +import { Router } from 'jsr:@oak/oak@^17/router' const router = new Router() @@ -357,7 +358,7 @@ app.listen({ port: 3000 }) ```ts -import { Hono } from 'jsr:@hono/hono' +import { Hono } from 'jsr:@hono/hono@^4' // You can set the basePath with Hono const functionName = 'tasks' @@ -368,9 +369,9 @@ app.get('/:id', async (c) => { const id = c.req.param('id') const task = {} // Fetch task by id here if (task) { - return new Response(JSON.stringify(task)) + return c.json({ task }) } else { - return new Response('Task not found', { status: 404 }) + return c.json({ error: 'Task not found' }, { status: 404 }) } }) @@ -381,9 +382,9 @@ app.patch('/:id', async (c) => { const task = {} // Fetch task by id here if (task) { Object.assign(task, updates) - return new Response(JSON.stringify(task)) + return c.json({ task }) } else { - return new Response('Task not found', { status: 404 }) + return c.json({ error: 'Task not found' }, { status: 404 }) } }) @@ -392,9 +393,9 @@ app.delete('/:id', async (c) => { const task = {} // Fetch task by id here if (task) { // Delete task - return new Response('Task deleted successfully') + return c.json({ message: 'Task deleted successfully' }) } else { - return new Response('Task not found', { status: 404 }) + return c.json({ error: 'Task not found' }, { status: 404 }) } }) diff --git a/apps/docs/content/guides/functions/unit-test.mdx b/apps/docs/content/guides/functions/unit-test.mdx index 0f5fec4f31e..97689b6f9fd 100644 --- a/apps/docs/content/guides/functions/unit-test.mdx +++ b/apps/docs/content/guides/functions/unit-test.mdx @@ -5,161 +5,199 @@ description: 'Writing Unit Tests for Edge Functions using Deno Test' subtitle: 'Writing Unit Tests for Edge Functions using Deno Test' --- -Testing is an essential step in the development process to ensure the correctness and performance of your Edge Functions. +Testing is an essential step in the development process to ensure the correctness, reliability, and performance of your Edge Functions. Because Edge Functions often combine HTTP handling, authentication, database access, and business logic, a good testing strategy gives you fast feedback and high confidence before deploying to production. + +In this guide you will learn how to write: + +- **Unit tests** for pure business logic such as pricing rules, calculations, etc. +- **Integration tests** for the full Edge Function by mocking at the network layer + +The examples and patterns shown here follow the same approaches used internally by Supabase's Edge Functions team. + +Deno ships with a fast, native test runner and excellent mocking utilities in `@std/testing`. See the [official Deno testing documentation](https://docs.deno.com/runtime/manual/basics/testing/) for more background. --- -## Testing in Deno +## The example scenario -Deno has a built-in test runner that you can use for testing JavaScript or TypeScript code. You can read the [official documentation](https://docs.deno.com/runtime/manual/basics/testing/) for more information and details about the available testing functions. +You can use a realistic Edge Function called `process-ticket` that calculates the final price of a ticket based on the authenticated user's age (loaded from the `profiles` table). + +**Business rules:** + +- Children aged 8 and under → free (`0`) +- Young people aged 9–17 → 20% discount +- Adults aged 18 and over → full price + +The function receives a JSON payload with a `price` field and returns `{ result: finalPrice }`. + +This example demonstrates common real-world requirements: + +- Request validation +- Authenticated database access via `withSupabase` +- Business rule application +- Proper error handling --- -## Folder structure +## Recommended project structure -We recommend creating your testing in a `supabase/functions/tests` directory, using the same name as the Function followed by `-test.ts`: - -```bash -└── supabase - ├── functions - │ ├── function-one - │ │ └── index.ts - │ └── function-two - │ │ └── index.ts - │ └── tests - │ └── function-one-test.ts # Tests for function-one - │ └── function-two-test.ts # Tests for function-two - └── config.toml ``` - ---- - -## Example - -The following script is a good example to get started with testing your Edge Functions: - -```typescript function-one-test.ts -// Import required libraries and modules -import { assert, assertEquals } from 'jsr:@std/assert@1' -import { createClient, SupabaseClient } from 'npm:@supabase/supabase-js@2' - -// Will load the .env file to Deno.env -import 'jsr:@std/dotenv/load' - -// Set up the configuration for the Supabase client -const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '' -const supabaseKey = Deno.env.get('SUPABASE_PUBLISHABLE_KEY') ?? '' -const options = { - auth: { - autoRefreshToken: false, - persistSession: false, - detectSessionInUrl: false, - }, -} - -// Test the creation and functionality of the Supabase client -const testClientCreation = async () => { - var client: SupabaseClient = createClient(supabaseUrl, supabaseKey, options) - - // Verify if the Supabase URL and key are provided - if (!supabaseUrl) throw new Error('supabaseUrl is required.') - if (!supabaseKey) throw new Error('supabaseKey is required.') - - // Test a query to the database - const { data: table_data, error: table_error } = await client - .from('my_table') - .select('*') - .limit(1) - if (table_error) { - throw new Error('Invalid Supabase client: ' + table_error.message) - } - assert(table_data, 'Data should be returned from the query.') -} - -// Test the 'hello-world' function -const testHelloWorld = async () => { - var client: SupabaseClient = createClient(supabaseUrl, supabaseKey, options) - - // Invoke the 'hello-world' function with a parameter - const { data: func_data, error: func_error } = await client.functions.invoke('hello-world', { - body: { name: 'bar' }, - }) - - // Check for errors from the function invocation - if (func_error) { - throw new Error('Invalid response: ' + func_error.message) - } - - // Log the response from the function - console.log(JSON.stringify(func_data, null, 2)) - - // Assert that the function returned the expected result - assertEquals(func_data.message, 'Hello bar!') -} - -// Register and run the tests -Deno.test('Client Creation Test', testClientCreation) -Deno.test('Hello-world Function Test', testHelloWorld) +supabase/ +├── functions/ +│ ├── _shared/ +│ │ └── types.ts # Database types +│ ├── process-ticket/ +│ │ ├── index.ts # Edge Function (uses withSupabase) +│ │ └── pricing.ts # Pure business logic (co-located) +│ └── tests/ +│ ├── utils/ +│ │ └── supabase_env.ts # Test helpers (env + JWT) +│ └── process-ticket/ +│ ├── pricing.test.ts # Unit tests for pricing +│ └── index.test.ts # Integration tests with fetch mocking +├── config.toml +└── deno.json ``` -This test case consists of two parts. - -1. The first part tests the client library and verifies that the database can be connected to and returns values from a table (`my_table`). -2. The second part tests the edge function and checks if the received value matches the expected value. Here's a brief overview of the code: - - We import various testing functions from the Deno standard library, including `assert`, `assertExists`, and `assertEquals`. - - We import the `createClient` and `SupabaseClient` classes from the `@supabase/supabase-js` library to interact with the Supabase client. - - We define the necessary configuration for the Supabase client, including the Supabase URL, API key, and authentication options. - - The `testClientCreation` function tests the creation of a Supabase client instance and queries the database for data from a table. It verifies that data is returned from the query. - - The `testHelloWorld` function tests the "Hello-world" Edge Function by invoking it using the Supabase client's `functions.invoke` method. It checks if the response message matches the expected greeting. - - We run the tests using the `Deno.test` function, providing a descriptive name for each test case and the corresponding test function. - -Make sure to replace the placeholders (`supabaseUrl`, `supabaseKey`, `my_table`) with the actual values relevant to your Supabase setup. +In this reference implementation the pricing logic lives inside the function folder +`process-ticket/pricing.ts`. You can also move it to `_shared/` if you want to reuse it across +multiple functions. +See the [Development Environment](/docs/guides/functions/development-environment) and [Managing dependencies](/docs/guides/functions/dependencies) guides for recommended `deno.json` and editor setup. + --- -## Running Edge Functions locally +## Unit tests: Testing pure business logic -To locally test and debug Edge Functions, use the Supabase CLI to run Edge Functions locally: +The pricing rules are pure functions with no side effects, so they are perfect candidates for fast, isolated unit tests. -1. Ensure that the Supabase server is running by executing the following command: +### The pricing module - ```bash - supabase start - ``` +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/process-ticket/pricing.ts" +title="Testing pure business logic | Implementation" +meta="supabase/functions/process-ticket/pricing.ts" +language="typescript" +/> -2. In your terminal, use the following command to serve the Edge Functions locally: +### Unit tests - ```bash - supabase functions serve - ``` +The reference implementation uses the BDD-style API from `@std/testing/bdd`: - This command starts a local server that runs your Edge Functions, enabling you to test and debug them in a development environment. +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/tests/process-ticket/pricing.test.ts" +title="Testing pure business logic | Unit-Test" +meta="supabase/functions/tests/process-ticket/pricing.test.ts" +language="typescript" +/> -3. Create the environment variables file: +Run the unit tests: - ```bash - # creates the file - touch .env - # adds the SUPABASE_URL secret - echo "SUPABASE_URL=http://localhost:54321" >> .env - # adds the SUPABASE_PUBLISHABLE_KEY secret - echo "SUPABASE_PUBLISHABLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" >> .env - # Alternatively, you can open it in your editor: - open .env - ``` +```bash +deno test supabase/functions/tests/process-ticket/pricing.test.ts +``` -4. To run the tests, use the following command in your terminal: +These tests run in milliseconds and give you immediate safety when changing discount rules. - ```bash - deno test --allow-all supabase/functions/tests/function-one-test.ts - ``` +--- + +## Integration tests: Testing the full Edge Function + +The reference implementation uses a pattern: **mocking `globalThis.fetch`** to intercept the Supabase REST calls made by the Edge Function. This approach requires **zero changes** to your production code for testability. + +### The Edge Function + +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/process-ticket/index.ts" +title="Testing the full Edge Function | Implementation" +meta="supabase/functions/process-ticket/index.ts" +language="typescript" +/> + +Key points: + +- Uses the high-level `withSupabase` helper from [`@supabase/server`](https://github.com/supabase/server) +- Automatically provides an authenticated `ctx.supabase` client +- Business logic is delegated to the co-located `pricing.ts` + +### Integration test setup + +This helper sets up a mock Supabase environment and generates valid RS256 JWTs for authenticated requests: + +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/tests/utils/supabase_env.ts" +title="Testing the full Edge Function | Unit-Test Utils" +meta="supabase/functions/tests/utils/supabase_env.ts" +language="typescript" +/> + +### Full integration tests + +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/tests/process-ticket/index.test.ts" +title="Testing the full Edge Function | Unit-Test" +meta="supabase/functions/tests/process-ticket/index.test.ts" +language="typescript" +/> + +Run the integration tests: + +```bash +deno test supabase/functions/tests/process-ticket/index.test.ts --allow-env +``` + +--- + +## Advantages of mocking approach + +This guide uses `fetch()` mock to demonstrate the following benefits: + +- Test the **real** Edge Function code path — no dependency injection needed in production code +- Simulate database responses, auth failures, network errors +- Keep your production Edge Function clean and focused +- Still get fast, deterministic tests that don't require a running Supabase instance + +This pattern fits great in higher-level helpers that you can control inner code, like `withSupabase`. + +--- + +## Running all tests + +Add to your `deno.json`: + +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/deno.json" +title="deno.json file" +meta="supabase/deno.json" +language="json" +lines={[[1,1], [6,-1]]} +/> + +Then: + +```bash +deno task test +``` + +--- + +## Best practices + +- Keep pure business logic in separate modules (even if co-located with the function) +- Use `withSupabase` + typed `Database` for clean, authenticated access +- Prefer mocking at the `fetch` boundary for integration tests when you don't want to modify production code +- Use `@std/testing/bdd` + `@std/testing/mock` for expressive, maintainable tests +- Generate realistic JWTs in tests when your function relies on authenticated Supabase clients +- Test both happy paths and error conditions (missing input, DB failures, invalid data) --- ## Resources -- Full guide on Testing Supabase Edge Functions on [Mansueli's tips](https://blog.mansueli.com/testing-supabase-edge-functions-with-deno-test) +- Read the [Deno testing guide](https://docs.deno.com/runtime/manual/basics/testing/) +- Learn more about [`withSupabase` and `@supabase/server`](/blog/introducing-supabase-server) +- See the other Edge Functions guides: [Development Environment](/docs/guides/functions/development-environment), [Managing dependencies](/docs/guides/functions/dependencies), [Deploy to Production](/docs/guides/functions/deploy) diff --git a/apps/docs/content/guides/functions/websockets.mdx b/apps/docs/content/guides/functions/websockets.mdx index 8129b4e2af6..d6a47758e65 100644 --- a/apps/docs/content/guides/functions/websockets.mdx +++ b/apps/docs/content/guides/functions/websockets.mdx @@ -5,7 +5,8 @@ description: 'How to handle WebSocket connections in Edge Functions' subtitle: 'Handle WebSocket connections in Edge Functions.' --- -Edge Functions supports hosting WebSocket servers that can facilitate bi-directional communications with browser clients. +Edge Functions supports hosting WebSocket servers that can facilitate bi-directional +communications with browser clients. This allows you to: @@ -13,7 +14,8 @@ This allows you to: - Create WebSocket relay servers for external APIs - Establish both incoming and outgoing WebSocket connections -For a production-ready reconnect pattern with session persistence and replay, see [Resumable WebSockets with Edge Functions](/docs/guides/functions/examples/resumable-websockets). +For a production-ready reconnect pattern with session persistence and replay, see +[Resumable WebSockets with Edge Functions](/docs/guides/functions/examples/resumable-websockets). --- @@ -36,7 +38,10 @@ export default { const upgrade = req.headers.get('upgrade') || '' if (upgrade.toLowerCase() != 'websocket') { - return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 }) + return Response.json( + { error: "request isn't trying to upgrade to WebSocket." }, + { status: 400 } + ) } const { socket, response } = Deno.upgradeWebSocket(req) @@ -61,7 +66,7 @@ export default { ```ts import { createServer } from 'node:http' -import { WebSocketServer } from 'npm:ws' +import { WebSocketServer } from 'npm:ws@^8' const server = createServer() // Since we manually created the HTTP server, @@ -105,7 +110,9 @@ server.listen(8080) You can also establish an outbound WebSocket connection to another server from an Edge Function. -Combining it with incoming WebSocket servers, it's possible to use Edge Functions as a WebSocket proxy, for example as a [relay server](https://github.com/supabase-community/openai-realtime-console?tab=readme-ov-file#using-supabase-edge-functions-as-a-relay-server) for the [OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/overview). +Combining it with incoming WebSocket servers, it's possible to use Edge Functions as a +WebSocket proxy, for example as a [relay server](https://github.com/supabase-community/openai-realtime-console?tab=readme-ov-file#using-supabase-edge-functions-as-a-relay-server) +for the [OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/overview). <$CodeSample external={true} @@ -121,11 +128,17 @@ lines={[[1, 3], [5, -1]]} ## Authentication -WebSocket browser clients don't have the option to send custom headers. Because of this, Edge Functions won't be able to perform the usual authorization header check to verify the JWT. +WebSocket browser clients don't have the option to send custom headers. Because of this, +Edge Functions won't be able to perform the usual authorization header check to verify +the JWT. -You can skip the default authorization header checks by explicitly providing `--no-verify-jwt` when serving and deploying functions. +You can skip the default authorization header checks by explicitly providing +`--no-verify-jwt` when serving and deploying functions. -To authenticate the user making WebSocket requests, you can pass the JWT in URL query params or via a custom protocol. The [`withSupabase`](/docs/guides/functions/auth) wrapper validates credentials on request headers, so it can't authenticate WebSocket clients. Verify the JWT yourself, as shown below. +To authenticate the user making WebSocket requests, you can pass the JWT in URL query +params or via a custom protocol. The [`withSupabase`](/docs/guides/functions/auth) +wrapper validates credentials on request headers, so it can't authenticate WebSocket +clients. Verify the JWT yourself, as shown below. ```ts -import { createClient } from 'npm:@supabase/supabase-js@2' +import { createClient } from 'npm:@supabase/supabase-js@^2' const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!) const supabase = createClient( @@ -150,7 +163,10 @@ export default { fetch: async (req) => { const upgrade = req.headers.get('upgrade') || '' if (upgrade.toLowerCase() != 'websocket') { - return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 }) + return Response.json( + { error: "request isn't trying to upgrade to WebSocket." }, + { status: 400 } + ) } // Please be aware query params may be logged in some logging systems. @@ -159,19 +175,19 @@ export default { if (!jwt) { console.error('Auth token not provided') - return new Response('Auth token not provided', { status: 403 }) + return Response.json({ error: 'Auth token not provided' }, { status: 403 }) } const { error, data } = await supabase.auth.getUser(jwt) if (error) { console.error(error) - return new Response('Invalid token provided', { status: 403 }) + return Response.json({ error: 'Invalid token provided' }, { status: 403 }) } if (!data.user) { console.error('user is not authenticated') - return new Response('User is not authenticated', { status: 403 }) + return Response.json({ error: 'User is not authenticated' }, { status: 403 }) } const { socket, response } = Deno.upgradeWebSocket(req) @@ -194,7 +210,7 @@ export default { ```ts -import { createClient } from 'npm:@supabase/supabase-js@2' +import { createClient } from 'npm:@supabase/supabase-js@^2' const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!) const supabase = createClient( @@ -207,7 +223,10 @@ export default { fetch: async (req) => { const upgrade = req.headers.get('upgrade') || '' if (upgrade.toLowerCase() != 'websocket') { - return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 }) + return Response.json( + { error: "request isn't trying to upgrade to WebSocket." }, + { status: 400 } + ) } // Sec-WebScoket-Protocol may return multiple protocol values `jwt-TOKEN, value1, value 2` @@ -219,18 +238,18 @@ export default { if (!jwt) { console.error('Auth token not provided') - return new Response('Auth token not provided', { status: 403 }) + return Response.json({ error: 'Auth token not provided' }, { status: 403 }) } const { error, data } = await supabase.auth.getUser(jwt) if (error) { console.error(error) - return new Response('Invalid token provided', { status: 403 }) + return Response.json({ error: 'Invalid token provided' }, { status: 403 }) } if (!data.user) { console.error('user is not authenticated') - return new Response('User is not authenticated', { status: 403 }) + return Response.json({ error: 'User is not authenticated' }, { status: 403 }) } const { socket, response } = Deno.upgradeWebSocket(req) @@ -254,17 +273,24 @@ export default { -The maximum duration is capped based on the wall-clock, CPU, and memory limits. The Function will shutdown when it reaches one of these [limits](/docs/guides/functions/limits). +The maximum duration is capped based on the wall-clock, CPU, and memory limits. The +Function will shutdown when it reaches one of these +[limits](/docs/guides/functions/limits). -When using WebSockets, keep in mind that the HTTP request is considered complete after `Deno.upgradeWebSocket(req)` returns the response. To prevent early worker retirement while the socket is still open, keep an unresolved `EdgeRuntime.waitUntil()` promise that resolves in `socket.onclose`. +When using WebSockets, keep in mind that the HTTP request is considered complete after +`Deno.upgradeWebSocket(req)` returns the response. To prevent early worker retirement +while the socket is still open, keep an unresolved `EdgeRuntime.waitUntil()` promise +that resolves in `socket.onclose`. --- ## Testing WebSockets locally -When testing Edge Functions locally with Supabase CLI, the instances are terminated automatically after a request is completed. This will prevent keeping WebSocket connections open. +When testing Edge Functions locally with Supabase CLI, the instances are terminated +automatically after a request is completed. This will prevent keeping WebSocket +connections open. To prevent that, you can update the `supabase/config.toml` with the following settings: @@ -275,6 +301,7 @@ policy = "per_worker" -When running with `per_worker` policy, Function won't auto-reload on edits. You will need to manually restart it by running `supabase functions serve`. +When running with `per_worker` policy, Function won't auto-reload on edits. You will +need to manually restart it by running `supabase functions serve`. diff --git a/apps/docs/content/guides/local-development/seeding-your-database.mdx b/apps/docs/content/guides/local-development/seeding-your-database.mdx index 4426ea1f7f8..0bce7ba4064 100644 --- a/apps/docs/content/guides/local-development/seeding-your-database.mdx +++ b/apps/docs/content/guides/local-development/seeding-your-database.mdx @@ -98,7 +98,29 @@ export default defineConfig({ Suppose you have a database with the following schema: -![An example schema](/docs/img/guides/cli/snaplet-example-schema.png) +```mermaid +erDiagram + User ||--o{ Post : createdBy + User ||--o{ Comment : userId + Post ||--o{ Comment : postId + User { + bigint id PK + text email + text name + } + Post { + bigint id PK + text title + text content + bigint createdBy FK + } + Comment { + bigint id PK + text text + bigint userId FK + bigint postId FK + } +``` You can use the seed script example generated by Snaplet `seed.ts` to define the values you want to generate. For example: @@ -107,8 +129,8 @@ You can use the seed script example generated by Snaplet `seed.ts` to define the - Three `Post.comments` from three different users. ```ts seed.ts -import { createSeedClient } from '@snaplet/seed' import { copycat } from '@snaplet/copycat' +import { createSeedClient } from '@snaplet/seed' async function main() { const seed = await createSeedClient({ dryRun: true }) diff --git a/apps/docs/content/guides/platform/backups.mdx b/apps/docs/content/guides/platform/backups.mdx index 9a2123b6f4c..620117f4e2c 100644 --- a/apps/docs/content/guides/platform/backups.mdx +++ b/apps/docs/content/guides/platform/backups.mdx @@ -97,7 +97,6 @@ Projects that want to use PITR must also use at least a Small compute add-on to size="medium" className="text-foreground-light mt-8 mb-6" > -
-
diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx index 032103c704c..83906fe29cd 100644 --- a/apps/docs/content/guides/platform/billing-faq.mdx +++ b/apps/docs/content/guides/platform/billing-faq.mdx @@ -96,11 +96,11 @@ We currently do not support annual plans officially. However, you can do a [cred #### What will happen when I exceed the Free Plan quota? -You will be notified when you exceed the Free Plan quota. It is important to take action at this point. If you continue to exceed the limits without reducing your usage, service restrictions will apply. To avoid service restrictions, you have two options: reduce your usage or upgrade to a paid plan. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section. +You will be notified when you exceed the Free Plan quota. It is important to take action at this point. If you continue to exceed the limits, service restrictions will apply. To avoid service restrictions, you can [manage your usage](/docs/guides/platform/manage-your-usage) or upgrade to a paid plan. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section. #### What will happen when I exceed the Pro Plan quota and have the spend cap on? -You will be notified when you exceed your Pro Plan quota. To unblock yourself, you can toggle off your spend cap in your [organization's billing settings](/dashboard/org/_/billing) to pay for over-usage beyond the Pro plans limits. If you continue to exceed the limits without reducing your usage or turning off the spend cap, restrictions will apply. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section. +You will be notified when you exceed your Pro Plan quota. To unblock yourself, you can toggle off your spend cap in your [organization's billing settings](/dashboard/org/_/billing) to pay for over-usage beyond the Pro plans limits. If you continue to exceed the limits without managing your usage or turning off the spend cap, restrictions will apply. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section. #### How do I scale beyond the limits of my Pro Plan? diff --git a/apps/docs/content/guides/platform/database-size.mdx b/apps/docs/content/guides/platform/database-size.mdx index f2e63601fb7..bf241988418 100644 --- a/apps/docs/content/guides/platform/database-size.mdx +++ b/apps/docs/content/guides/platform/database-size.mdx @@ -114,6 +114,14 @@ Free Plan projects enter [read-only](#read-only-mode) mode when your **database - [Upgrade to the Pro Plan](/dashboard/org/_/billing) to increase the database size quota. [Disable the Spend Cap](https://app.supabase.com/org/_/billing?panel=costControl) if you want your Pro instance to auto-scale beyond the 8 GB disk size limit. - [Disable read-only mode](#disabling-read-only-mode) and reduce your database size. +### Fair use database size restriction + +Separate from the per-project read-only mode above, your organization can be placed under a [Fair Use](/docs/guides/platform/billing-faq#fair-use-policy) service restriction (requests return a `402` status code) when its database size exceeds the plan quota. This quota is evaluated **per organization**, summing the database size across all of your projects. + +Importantly, it is based on the **average daily database size over the billing period**, not the live size. Reducing your database size does not immediately lift the restriction: the average stays elevated until enough lower-usage days accumulate, and it effectively resets when your billing cycle rolls over. This is why a project that is well under the limit today can still be restricted, as its average across the period is still over. + +To resolve it, upgrade your plan or disable your Spend Cap to lift the restriction immediately. Otherwise, reduce your database size and wait for the new billing cycle, at which point the average restarts from your current size. + ### Read-only mode In some cases Supabase may put your database into read-only mode to prevent your database from exceeding the billing or disk limitations. diff --git a/apps/docs/content/guides/platform/hipaa-projects.mdx b/apps/docs/content/guides/platform/hipaa-projects.mdx index 2e91554e454..78f1864bce1 100644 --- a/apps/docs/content/guides/platform/hipaa-projects.mdx +++ b/apps/docs/content/guides/platform/hipaa-projects.mdx @@ -24,5 +24,6 @@ These include: - Enabling [Point in Time Recovery](/docs/guides/platform/backups#point-in-time-recovery) which requires at least a [small compute add-on](/docs/guides/platform/compute-add-ons). - Turning on [SSL Enforcement](/docs/guides/platform/ssl-enforcement). - Enabling [Network Restrictions](/docs/guides/platform/network-restrictions). +- Keeping [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) enabled. Additional security checks and controls will be added as the security advisor is extended and additional security controls are made available. diff --git a/apps/docs/content/guides/platform/ipv4-address.mdx b/apps/docs/content/guides/platform/ipv4-address.mdx index 7dc01f9765b..f17be34c3cd 100644 --- a/apps/docs/content/guides/platform/ipv4-address.mdx +++ b/apps/docs/content/guides/platform/ipv4-address.mdx @@ -77,7 +77,7 @@ By default, Supabase Postgres use IPv6 addresses. If your system doesn't support ### Checking your network IPv6 support -You can check if your personal network is IPv6 compatible at https://test-ipv6.com. +You can check if your personal network is IPv6 compatible at https://ipv6test.google.com/. ### Checking platforms for IPv6 support: diff --git a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx index 4469a946e73..b05dbb00359 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx @@ -63,6 +63,12 @@ Cached and uncached egress have independent quotas and independent pricing. Cach Egress is charged by gigabyte. Charges apply only for usage exceeding your subscription plan's quota. This quota is called the Unified Egress Quota because it can be used across all services (Database, Auth, Storage etc.). + + +Egress accumulates over the billing cycle and resets at the start of the next cycle. Usage that has already been served cannot be reduced retroactively, so the optimizations below lower future egress only. If your organization is restricted for egress, the restriction clears at the start of the next billing cycle, or immediately if you upgrade your plan or disable your Spend Cap. + + + ### Usage on your invoice Usage is shown as "Egress GB" and "Cached Egress GB" on your invoice. diff --git a/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx b/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx index eca3dd5544c..b2dfc468eff 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx @@ -12,6 +12,8 @@ You are charged for the total size of all assets in your buckets. Storage size is charged by Gigabyte-Hours (GB-Hrs). 1 GB-Hr represents the use of 1 GB of storage for 1 hour. For example, storing 10 GB of data for 5 hours results in 50 GB-Hrs (10 GB × 5 hours). +Because usage is measured in GB-Hrs, your Storage size for quota and billing is effectively the average across the billing period, not the live size. For example, storing 20 GB for the first half of the month and 0 GB for the second half averages to 10 GB. This means reducing storage late in the cycle lowers the average only gradually, so it may not immediately clear a restriction until the next billing cycle begins. + ### Usage on your invoice Usage is shown as "Storage Size GB-Hrs" on your invoice. diff --git a/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx b/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx index 8bcf027f606..744d61ee7c5 100644 --- a/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx +++ b/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx @@ -4,9 +4,9 @@ subtitle: 'Learn how to backup and restore projects using the Supabase CLI' breadcrumb: 'Migrations' --- -# Migrating the database +## Migrating the database -## Backup database using the CLI +### Back up database using the CLI @@ -74,7 +74,7 @@ breadcrumb: 'Migrations' -## Before you begin +### Before you begin -
- - <$Partial path="postgres_installation.mdx" /> - -
+ + <$Partial path="postgres_installation.mdx" /> +
-## Restore backup using CLI +### Restore backup using CLI @@ -197,9 +195,9 @@ breadcrumb: 'Migrations' -## Special considerations +### Special considerations -#### Preserving migration history +##### Preserving migration history If you were using Supabase CLI for managing migrations on your old database and would like to preserve the migration history in your newly restored project, you need to insert the migration records separately using the following commands. @@ -214,7 +212,7 @@ psql \ --dbname "$NEW_DB_URL" ``` -#### Schema changes to `auth` and `storage` +##### Schema changes to `auth` and `storage` If you have modified the `auth` and `storage` schemas in your old project, such as adding triggers or Row Level Security(RLS) policies, you have to restore them separately. The Supabase CLI can help you diff the changes to these schemas using the following commands. @@ -223,13 +221,13 @@ supabase link --project-ref "$OLD_PROJECT_REF" supabase db diff --linked --schema auth,storage > changes.sql ``` -## Troubleshooting notes +### Troubleshooting notes -#### Disabling triggers during restore: +##### Disabling triggers during restore: Setting `session_replication_role` to `replica` disables triggers during the migration, preventing columns from being double encrypted. -#### Custom roles require passwords +##### Custom roles require passwords If you created any [custom roles](/dashboard/project/_/database/roles) with the `LOGIN` attribute, you must manually set their passwords in the new project. This can be done with the SQL command: @@ -237,7 +235,7 @@ If you created any [custom roles](/dashboard/project/_/database/roles) with the alter user "YOUR_USER" with password 'SOME_NEW_PASSWORD'; ``` -#### `supabase_admin` permission errors +##### `supabase_admin` permission errors If you encounter permission errors related to `supabase_admin` during restore: @@ -248,7 +246,7 @@ If you encounter permission errors related to `supabase_admin` during restore: ALTER ... OWNER TO "supabase_admin" ``` -#### `cli_login_postgres` role grant error +##### `cli_login_postgres` role grant error If you encounter the error: @@ -264,7 +262,7 @@ DETAIL: Only roles with the ADMIN option on role "postgres" may grant this role GRANT "postgres" TO "cli_login_postgres" WITH INHERIT FALSE GRANTED BY "supabase_admin"; ``` -#### `cli_login_postgres` role issues after cloning +##### `cli_login_postgres` role issues after cloning The `cli_login_role` must be created by the `supabase_admin` role. If the migration process cloned over the role before the CLI could generate its own version, it may encounter the error: @@ -279,9 +277,9 @@ To resolve the issue, drop the custom `cli_login_postgres` role. Then the CLI ca DROP ROLE IF EXISTS cli_login_postgres; ``` -# Migrating edge functions +## Migrating edge functions -## Steps (using the Supabase CLI): +### Steps (using the Supabase CLI): @@ -326,7 +324,7 @@ DROP ROLE IF EXISTS cli_login_postgres; -## Steps (using the Supabase Dashboard): +### Steps (using the Supabase Dashboard): @@ -369,7 +367,7 @@ Dependencies defined through [import maps](/docs/guides/functions/dependencies#u -# Migrating storage objects +## Migrating storage objects @@ -808,6 +806,6 @@ Dependencies defined through [import maps](/docs/guides/functions/dependencies#u -## Resources +### Resources - [Connecting with PSQL](/docs/guides/database/psql) diff --git a/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx b/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx index 3e59cdeb9a7..f403e599b68 100644 --- a/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx +++ b/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx @@ -20,17 +20,13 @@ Dashboard backups are only available for older projects that still use logical b size="medium" className="text-foreground-light mt-8 mb-6" > -
- <$Partial path="postgres_installation.mdx" /> - -
-
- @@ -52,7 +48,6 @@ Dashboard backups are only available for older projects that still use logical b -
## Things to keep in mind diff --git a/apps/docs/content/guides/platform/postgres-connection-logging.mdx b/apps/docs/content/guides/platform/postgres-connection-logging.mdx new file mode 100644 index 00000000000..273cb11a122 --- /dev/null +++ b/apps/docs/content/guides/platform/postgres-connection-logging.mdx @@ -0,0 +1,81 @@ +--- +id: 'postgres-connection-logging' +title: 'Postgres connection logging' +description: 'Enable or disable Postgres connection logging for audit and compliance.' +--- + +For security monitoring and compliance audits, Postgres can log connection lifecycle events to your project's [Postgres logs](/docs/guides/telemetry/logs#postgres), including events such as `connection received`, `connection authenticated`, and `connection authorized`. + +## Default behavior + +By default, Supabase sets `log_connections` to off for new projects and you must enable it first. This behavior matches common managed Postgres defaults and reduces log volume from high-frequency connection events. + +<$Partial path="log_connections_default_effective_date.mdx" /> + +Existing projects may retain different settings depending on plan and compliance configuration: + +- **Team, Enterprise, and HIPAA organizations** — Connection logging is typically enabled to support audit requirements. +- **HIPAA projects** — Supabase enables connection logging when a project is marked as high compliance. The [Security Advisor](/dashboard/project/_/advisors/security) warns if connection logging is later disabled. + +## Compliance considerations + + + +If you need connection audit evidence for SOC 2 or other compliance programs, you must enable it explicitly. + + + +Connection logging supports audit and monitoring controls required by some compliance programs: + +- **HIPAA** — High-compliance projects should keep connection logging enabled. See the [shared responsibility model for healthcare data](/docs/guides/deployment/shared-responsibility-model#managing-healthcare-data) and [HIPAA compliance guide](/docs/guides/security/hipaa-compliance). +- **SOC 2** — Users who need connection audit evidence should enable logging and retain logs according to their own policies. See the [SOC 2 compliance guide](/docs/guides/security/soc-2-compliance). + +Disabling connection logging does not affect other Supabase logging (for example, [Platform Audit Logs](/docs/guides/security/platform-audit-logs), [Auth Audit Logs](/docs/guides/auth/audit-logs), or [pgAudit](/docs/guides/telemetry/logs#configuring-pgauditlog)). + +## Manage connection logging via the dashboard + +You can configure connection logging from the **Log connections** setting in the [Database Settings](/dashboard/project/_/database/settings) section of the Dashboard. + +Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. + + + +Connection events appear in Postgres logs. In the [Logs Explorer](/dashboard/project/_/logs-explorer), connection lifecycle messages may be hidden by default to reduce noise. Use the connection logs filter in the sidebar to show or hide them. + + + +## Manage connection logging via the Management API + +You can also manage connection logging using the [Management API](/docs/reference/api/v1-update-postgres-config): + +```bash +# Get your access token from https://supabase.com/dashboard/account/tokens +export SUPABASE_ACCESS_TOKEN="your-access-token" +export PROJECT_REF="your-project-ref" + +# Get current Postgres config +curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/config/database/postgres" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" + +# Enable connection logging +curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/config/database/postgres" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "log_connections": true + }' + +# Disable connection logging +curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/config/database/postgres" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "log_connections": false + }' +``` + +To verify the setting, use the SQL Editor: + +```sql +show log_connections; +``` diff --git a/apps/docs/content/guides/platform/read-replicas.mdx b/apps/docs/content/guides/platform/read-replicas.mdx index 2fb83ea8c4f..212ee1bea9a 100644 --- a/apps/docs/content/guides/platform/read-replicas.mdx +++ b/apps/docs/content/guides/platform/read-replicas.mdx @@ -38,23 +38,31 @@ You can only read data from a Read Replica. This is in contrast to a Primary dat size="large" className="text-foreground-light mt-8 mb-6" > -
- When your database starts slowing down, you face a choice: make your existing database bigger (scale vertically), or spread the load across multiple databases (scale horizontally). Both approaches work. Neither is universally correct. The right answer depends on your workload, your budget, and where the bottleneck actually is. - +```mermaid +flowchart TD + A[Database slowing down] --> B{CPU above 70% sustained?} + B -->|No| C[Monitor, do not scale yet] + B -->|Yes| D{Queries optimized? Indexes in place?} + D -->|No| E[Run EXPLAIN ANALYZE
Add missing indexes
Optimize first] + E --> D + D -->|Yes| F{Workload 80%+ reads?} + F -->|No| G[Upgrade compute
Replicas will not help writes] + F -->|Yes| H{Already at 16XL?} + H -->|Yes| I[Read Replicas
Only horizontal option left] + H -->|No| J{Need workload isolation
or geo-distribution?} + J -->|Yes| K[Read Replicas] + J -->|No| L[Either works
Compute is simpler
Replicas scale further] +```
-
- ## Features diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index 25d87e3ebc7..d78437d7917 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -189,3 +189,83 @@ select graphql.resolve('{ __schema { queryType { name } } }'); ``` Existing projects on pg_graphql 1.5.x are not impacted unless they choose to upgrade. + +### Ltree indexes require reindexing after upgrade + +_Applies when upgrading to Postgres 15.18 or 17.10._ + + + +You are affected only if you have indexes on `ltree` columns and your database uses a multibyte encoding or a non-`libc` collation provider. + + + +After upgrading, indexes on `ltree` columns that were built under the previous version can return incomplete results until the index is rebuilt. For example, label searches silently miss rows that are present. This affects databases using a multibyte encoding, such as UTF-8, or a non-`libc` collation provider such as ICU or builtin. + +To mitigate this issue: + +1. Check whether your database needs reindexing: + + ```sql + select + pg_encoding_to_char(encoding) as encoding, + pg_encoding_max_length(encoding) as max_bytes_per_char, -- 1 = single-byte, >1 = multibyte + datlocprovider as collation_provider, -- 'c' libc, 'i' icu, 'b' builtin + (pg_encoding_max_length(encoding) > 1 or datlocprovider != 'c') as reindex_required + from pg_database + where datname = current_database(); + ``` + + If `reindex_required` is `false`, such as a single-byte encoding like LATIN1 with `libc` collation, no action is needed. + +2. If `reindex_required` is `true`, find the affected indexes: + + ```sql + select schemaname, tablename, indexname + from pg_indexes + where + indexname in ( + select c.relname + from + pg_index as i + join pg_class as c on i.indexrelid = c.oid + join pg_attribute as a on a.attrelid = i.indrelid and a.attnum = ANY(i.indkey) + join pg_type as t on a.atttypid = t.oid + where t.typname in ('ltree', '_ltree') + ); + ``` + +3. Reindex each affected index. `REINDEX INDEX CONCURRENTLY` runs online with no downtime: + + ```sql + REINDEX INDEX CONCURRENTLY ; + ``` + +### Custom operator selectivity estimators + +_Applies when upgrading to Postgres 15.18 or 17.10._ + +Attaching a non-built-in (extension- or user-provided) selectivity estimator function to an operator now requires superuser. Existing operators continue to work — the check only fires when an operator is (re)created, most commonly during `pg_dump` / `pg_restore`, a logical restore, or a branch. + +Because Supabase database roles are not superusers, recreating such an operator on your behalf (for example during a restore or branch) can fail with: + +``` +ERROR: must be superuser to specify a non-built-in restriction estimator function +``` + +Most projects are not affected. To check whether your database has any user-defined operators that reference a non-built-in estimator: + +```sql +SELECT n.nspname AS schema, o.oprname AS operator +FROM pg_operator o +JOIN pg_namespace n ON o.oprnamespace = n.oid +WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') + AND ((o.oprrest <> 0 AND o.oprrest::oid >= 10000) + OR (o.oprjoin <> 0 AND o.oprjoin::oid >= 10000)) + AND NOT EXISTS ( + SELECT 1 FROM pg_depend d + WHERE d.classid = 'pg_operator'::regclass AND d.objid = o.oid AND d.deptype = 'e' + ); +``` + +If this returns no rows, your project is unaffected. diff --git a/apps/docs/content/guides/queues/quickstart.mdx b/apps/docs/content/guides/queues/quickstart.mdx index 58af639d01c..ce44b7fda84 100644 --- a/apps/docs/content/guides/queues/quickstart.mdx +++ b/apps/docs/content/guides/queues/quickstart.mdx @@ -3,7 +3,6 @@ title: Quickstart subtitle: 'Learn how to use Supabase Queues to add and read messages' --- -{/* */} This guide is an introduction to interacting with Supabase Queues via the Dashboard and official client library. Check out [Queues API Reference](/docs/guides/queues/api) for more details on our API. ## Concepts @@ -25,8 +24,6 @@ Supabase Queues offers three types of Queues: - **Basic Queue**: A durable Queue that stores Messages in a logged table. - **Unlogged Queue**: A transient Queue that stores Messages in an unlogged table for better performance but may result in loss of Queue Messages. -- **Partitioned Queue** (_Coming Soon_): A durable and scalable Queue that stores Messages in multiple table partitions for better performance. - ## Create Queues To get started, navigate to the [Supabase Queues](/dashboard/project/_/integrations/queues/overview) Postgres Module under Integrations in the Dashboard and enable the `pgmq` extension. @@ -40,8 +37,8 @@ To get started, navigate to the [Supabase Queues](/dashboard/project/_/integrati - -If you've already created a Queue click the **Create a queue** button instead. - -
+- Click **Create queue** button - Name your queue - + Queue names can only be lowercase and hyphens and underscores are permitted. - Select your [Queue Type](#queue-types) +- We recommend leaving Row Level Security (RLS) enabled. With it enabled, you don't need to set additional RLS on the queue tables. -### What happens when you create a queue? + Every new Queue creates two tables in the `pgmq` schema. These tables are `pgmq.q_` to store and process active messages and `pgmq.a_` to store any archived messages. -A "Basic Queue" will create `pgmq.q_` and `pgmq.a_` tables as logged tables. +A "Basic Queue" creates `pgmq.q_` and `pgmq.a_` tables as logged tables. -However, an "Unlogged Queue" will create `pgmq.q_` as an unlogged table for better performance while sacrificing durability. The `pgmq.a_` table will still be created as a logged table so your archived messages remain safe and secure. +However, an "Unlogged Queue" creates `pgmq.q_` as an unlogged table for better performance while sacrificing durability. The `pgmq.a_` table is still created as a logged table so your archived messages remain safe and secure. + + ## Expose Queues to client-side consumers -Queues, by default, are not exposed over Supabase Data API and are only accessible via Postgres clients. +Queues, by default, are not exposed over the Supabase Data API and are only accessible via Postgres clients. However, you may grant client-side consumers access to your Queues by enabling the Supabase Data API and granting permissions to the Queues API, which is a collection of database functions in the `pgmq_public` schema that wraps the database functions in the `pgmq` schema. This is to prevent direct access to the `pgmq` schema and its tables (RLS is not enabled by default on any tables) and database functions. -To get started, navigate to the Queues [Settings page](/dashboard/project/_/integrations/queues/settings) and toggle on “Expose Queues via PostgREST”. Once enabled, Supabase creates and exposes a `pgmq_public` schema containing database function wrappers to a subset of `pgmq`'s database functions. +To get started, navigate to the [**Queues > Settings**](/dashboard/project/_/integrations/queues/settings) section of the Dashboard and enable **Expose Queues via PostgREST**. Once enabled, Supabase creates and exposes a `pgmq_public` schema containing database function wrappers to a subset of `pgmq`'s database functions. - +If you expose your pgmq schema with the Data API, for security purposes, you must enable Row Level Security (RLS) on all Queue tables (all tables in `pgmq` schema that begin with `q_`) -### Enable RLS on your tables in `pgmq` schema - -For security purposes, you must enable Row Level Security (RLS) on all Queue tables (all tables in `pgmq` schema that begin with `q_`) if the Data API is enabled. - -You’ll want to create RLS policies for any Queues you want your client-side consumers to interact with. - - +Add an RLS policy for any Queues you want your client-side consumers to interact with, by clicking the _Add RLS Policy_ button on [the overview page of any Queue in the Dashboard](/dashboard/project/_/integrations/queues/queues). ### Grant permissions to `pgmq_public` database functions @@ -139,13 +111,13 @@ The permissions required for each Queue API database function: | `read` `pop` | `Select` `Update` | | `archive` `delete` | `Select` `Delete` | -To manage your queue permissions, click on the Queue Settings button. +To manage your queue permissions, click on the Queue Settings cog button on [the overview page of any Queue in the Dashboard](/dashboard/project/_/integrations/queues/queues). + - - -`postgres` and `service_role` roles should never be exposed client-side. +You should never expose `postgres` and `service_role` roles client-side. ### Enqueueing and dequeueing messages -Once your Queue has been created, you can begin enqueueing and dequeueing Messages. +Once you have created your Queue, you can begin enqueueing and dequeueing Messages. + **How often is Supabase audited?** Supabase undergoes annual audits. The HIPAA controls are audited during the same audit period as the SOC 2 controls. @@ -64,3 +70,4 @@ Supabase undergoes annual audits. The HIPAA controls are audited during the same 5. [Configuring HIPAA projects](/docs/guides/platform/hipaa-projects) on Supabase 6. [Shared Responsibility Model](/docs/guides/deployment/shared-responsibility-model) 7. [HIPAA shared responsibility](/docs/guides/deployment/shared-responsibility-model#managing-healthcare-data) +8. [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) diff --git a/apps/docs/content/guides/security/platform-audit-logs.mdx b/apps/docs/content/guides/security/platform-audit-logs.mdx index 00818393291..1ead782aa0d 100644 --- a/apps/docs/content/guides/security/platform-audit-logs.mdx +++ b/apps/docs/content/guides/security/platform-audit-logs.mdx @@ -3,7 +3,9 @@ title: 'Platform Audit Logs' description: 'Monitor and track organization member activities via platform API or dashboard.' --- -Any [Platform API](/docs/reference/api/introduction) or [dashboard](/dashboard) actions performed by organization members are logged automatically for auditing and security purposes. This includes actions such as creating a new project, inviting members, modifying an edge function or changing project settings. +This topic covers how to view and stream Platform Audit Logs for your organization. + +Any [Platform API](/docs/reference/api/introduction) or [dashboard](/dashboard) actions performed by organization members are logged automatically for auditing and security purposes. This includes actions such as creating a new project, inviting members, modifying an edge function or changing project settings. You can view these logs in the dashboard or stream them to an external destination using [Audit Log Drains](#accessing-audit-log-drains). Besides Platform Audit Logs, Supabase Auth also provides [Auth Audit Logs](/docs/guides/auth/audit-logs) to monitor authentication-related activities within your projects. @@ -42,8 +44,11 @@ For each audit log, you can see additional details by clicking on the log entry: Each Supabase user account also has access to [Account Audit logs](/dashboard/account/audit) which displays these logs for only the associated user account. +## Accessing Audit Log Drains + +Audit Log Drains can be configured under your [organization's audit log drains](/dashboard/org/_/audit-log-drains). For setup instructions and supported destinations, see the [Log Drains guide](/docs/guides/telemetry/log-drains). + ## Limitations - There is currently no way to export the logs via dashboard -- There is currently no way to set up a log drain of platform audit logs - Retention periods depend on your plan diff --git a/apps/docs/content/guides/security/product-security.mdx b/apps/docs/content/guides/security/product-security.mdx index 08600a1d290..f47e9b1d1c2 100644 --- a/apps/docs/content/guides/security/product-security.mdx +++ b/apps/docs/content/guides/security/product-security.mdx @@ -23,6 +23,7 @@ Various products at Supabase have their own hardening and configuration guides, - [Custom claims and role based access control](/docs/guides/api/custom-claims-and-role-based-access-control-rbac) - [Managing Postgres roles](/docs/guides/database/postgres/roles) - [Managing secrets with Vault](/docs/guides/database/vault) +- [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) - [Superuser access and unsupported operations](docs/guides/database/postgres/roles-superuser) ## Storage diff --git a/apps/docs/content/guides/security/soc-2-compliance.mdx b/apps/docs/content/guides/security/soc-2-compliance.mdx index d9fa83916ba..e982263ab2c 100644 --- a/apps/docs/content/guides/security/soc-2-compliance.mdx +++ b/apps/docs/content/guides/security/soc-2-compliance.mdx @@ -21,32 +21,35 @@ Our [HIPAA documentation](/docs/guides/security/hipaa-compliance) provides more -# Meeting compliance requirements +## Meeting compliance requirements SOC 2 compliance is a critical aspect of data security for Supabase and our customers. Being fully SOC 2 compliant is a shared responsibility and here’s a breakdown of the responsibilities for both parties: -### Supabase responsibilities +#### Supabase responsibilities 1. **Security Measures**: Supabase implements robust security controls to protect customer data. These includes measures to prevent data breaches and ensure the confidentiality and integrity of the information managed and stored by the platform. Supabase is obliged to be vigilant about security risks and must demonstrate that our security measures meet industry standards through regular audits. 2. **Compliance Audits**: Supabase undergoes SOC 2 audits yearly to verify that our data management practices comply with the Trust Services Criteria (TSC), which include security, availability, processing integrity, confidentiality, and privacy. These audits are conducted by an independent third party. 3. **Incident Response**: Supabase has an incident response plan in place to handle data breaches efficiently. This plan outlines how the organization detects issues, responds to incidents, and manages system vulnerabilities. 4. **Reporting**: Upon a successful audit, Supabase receive a SOC 2 report that details our compliance status. This report is available to customers as a SOC 2 Type 2 report, and allows customers and stakeholders to assure that Supabase has implemented adequate and the requisite safeguards to protect sensitive information. -### Customer responsibilities +#### Customer responsibilities 1. **Compliance Requirements**: Understand your own compliance requirements. While SOC 2 compliance is not a legal requirement, many enterprise customers require their providers to have a SOC 2 report. This is because it provides assurance that the provider has implemented robust controls to protect customer data. 2. **Due Diligence**: Customers must perform due diligence when selecting Supabase as a provider. This includes reviewing the SOC 2 Type 2 report to ensure that Supabase meets the expected security standards. Customers should also understand the division of responsibilities between themselves and Supabase to avoid duplication of effort. 3. **Monitoring and Review**: Customers should regularly monitor and review Supabase’s compliance status. 4. **Control Compliance**: If a customer needs to be SOC 2 compliant, they should themselves implement the requisite controls and undergo a SOC 2 audit. +5. **Audit logging**: Supabase sets [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) to off by default for new projects. If your SOC 2 program requires connection audit evidence, enable connection logging and define how you retain and review those logs. -### Shared responsibilities +<$Partial path="log_connections_default_effective_date.mdx" /> + +#### Shared responsibilities 1. **Data Security**: Both customers and Supabase share the responsibility of ensuring data security. While the Supabase, as the provider, implements the security controls, the customer must ensure that their use of the Supabase platform does not compromise these controls. 2. **Control Compliance**: Supabase asserts through our SOC 2 that all requisite security controls are met. Customers wishing to also be SOC 2 compliant need to go through their own SOC 2 audit, verifying that security controls are met on the customer's side. In summary, SOC 2 compliance involves a shared responsibility between Supabase and our customers to ensure the security and integrity of data. Supabase, as a provider, must implement and maintain robust security measures, customers must perform due diligence and monitor Supabase's compliance status, while also implement their own compliance controls to protect their sensitive information. -## Frequently asked questions +### Frequently asked questions **How often is Supabase SOC 2 audited?** @@ -78,7 +81,8 @@ While SOC 2 itself does not mandate specific data residency requirements, organi SOC 2 is non-industry specific and provides a framework for the security and privacy of data. This is however not sufficient in most cases when dealing with Protected Healthcare Information (PHI), which requires additional privacy and legal controls. When dealing with PHI in the United States or for United States customers, HIPAA is mandatory. -## Resources +### Resources 1. [System and Organization Controls: SOC Suite of Services](https://www.aicpa-cima.com/resources/landing/system-and-organization-controls-soc-suite-of-services) 2. [Shared Responsibility Model](/docs/guides/deployment/shared-responsibility-model) +3. [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) diff --git a/apps/docs/content/guides/storage/schema/design.mdx b/apps/docs/content/guides/storage/schema/design.mdx index f2d380a8dd4..e0f06401c90 100644 --- a/apps/docs/content/guides/storage/schema/design.mdx +++ b/apps/docs/content/guides/storage/schema/design.mdx @@ -18,7 +18,37 @@ This is important because the storage schema only stores the metadata and the ac Here is the schema that represents the Storage service: -Storage schema design +```mermaid +erDiagram + buckets ||--o{ objects : "buckets_id:id" + buckets { + text id PK + text name + timestamptz created_at + timestamptz updated_at + boolean public + bigint file_size_limit + text[] allowed_mime_types + text owner_id + } + objects { + uuid id PK + text bucket_id FK + text name + timestamptz created_at + timestamptz updated_at + jsonb metadata + text[] path_tokens + text version + text owner_id + } + migrations { + integer id PK + varchar(100) name + varchar(40) hash + timestamp executed_at + } +``` You have the option to query this table directly to retrieve information about your files in Storage without the need to go through our API. diff --git a/apps/docs/content/guides/telemetry/log-drains.mdx b/apps/docs/content/guides/telemetry/log-drains.mdx index c2c7d069aef..a00f0519eff 100644 --- a/apps/docs/content/guides/telemetry/log-drains.mdx +++ b/apps/docs/content/guides/telemetry/log-drains.mdx @@ -8,7 +8,7 @@ Log drains send all logs of the Supabase stack to one or more desired destinatio You can read about the initial announcement [here](/blog/log-drains) and vote for your preferred drains in [this discussion](https://github.com/orgs/supabase/discussions/28324?sort=top). -# Supported destinations +## Supported destinations The following table lists the supported destinations and the required setup configuration: @@ -23,7 +23,7 @@ The following table lists the supported destinations and the required setup conf HTTP requests are batched with a max of 250 logs or 1 second intervals, whichever happens first. Logs are compressed via Gzip if the destination supports it. -## Generic HTTP endpoint +### Generic HTTP endpoint Logs are sent as a POST request with a JSON body. Both HTTP/1 and HTTP/2 protocols are supported. Custom headers can optionally be configured for all requests. @@ -138,7 +138,7 @@ Deno.serve(async (req) => { -## Datadog logs +### Datadog logs Logs sent to Datadog have the name of the log source set on the `service` field of the event and the source set to `Supabase`. Logs are gzipped before they are sent to Datadog. @@ -187,7 +187,7 @@ To setup Datadog log drain, generate a Datadog API key [here](https://app.datado If you are interested in other log drains, upvote them [here](https://github.com/orgs/supabase/discussions/28324) -## Loki +### Loki Logs sent to the Loki HTTP API are specifically formatted according to the HTTP API requirements. See the official Loki HTTP API documentation for [more details](https://grafana.com/docs/loki/latest/reference/loki-http-api/#ingest-logs). @@ -199,7 +199,7 @@ The `event_message` and `timestamp` fields will be dropped from the events to av Loki must be configured to accept **structured metadata**, and it is advised to increase the default maximum number of structured metadata fields to at least 500 to accommodate large log event payloads of different products. -## Sentry +### Sentry Logs are sent to Sentry as part of [Sentry's Logging Product](https://docs.sentry.io/product/explore/logs/). Ingesting Supabase logs as Sentry errors is currently not supported. @@ -213,7 +213,7 @@ All fields from the log event are attached as attributes to the Sentry log, whic If you are self-hosting Sentry, Sentry Logs are only supported in self-hosted version [25.9.0](https://github.com/getsentry/self-hosted/releases/tag/25.9.0) and later. -## Axiom +### Axiom Logs sent to a specified Axiom's dataset as JSON of a raw log event, with timestamp modified to be parsed by ingestion endpoint. @@ -227,7 +227,7 @@ To set up the Axiom log drain, you have to: - API token 4. Watch for events in the Stream panel of Axiom Console -## Amazon S3 +### Amazon S3 Logs are written to an existing S3 bucket that you own. @@ -245,7 +245,7 @@ Ensure the AWS account tied to the Access Key ID has permissions to write to the -## OpenTelemetry protocol (OTLP) +### OpenTelemetry protocol (OTLP) Logs are sent to any OTLP-compatible endpoint using the OpenTelemetry Protocol over HTTP with Protocol Buffers encoding. @@ -352,6 +352,6 @@ Refer to your observability platform's documentation for specific authentication -## Pricing +### Pricing For a detailed breakdown of how charges are calculated, refer to [Manage Log Drain usage](/docs/guides/platform/manage-your-usage/log-drains). diff --git a/apps/docs/content/guides/telemetry/logs.mdx b/apps/docs/content/guides/telemetry/logs.mdx index 60fec5f516f..0d41674f528 100644 --- a/apps/docs/content/guides/telemetry/logs.mdx +++ b/apps/docs/content/guides/telemetry/logs.mdx @@ -143,6 +143,16 @@ Do not log Personal Identifiable Information (PII) within the `User-Agent` heade
+## Logging Postgres connections + +Postgres can log connection lifecycle events to your project's Postgres logs, for example when a client connects or authenticates. By default, Supabase sets `log_connections` to off for new projects and you must enable it first. + +<$Partial path="log_connections_default_effective_date.mdx" /> + +To enable connection logging for audit or compliance, see [Postgres connection logging](/docs/guides/platform/postgres-connection-logging). + +In the [Logs Explorer](/dashboard/project/_/logs-explorer), connection lifecycle messages may be hidden by default. Use the connection logs filter in the sidebar to show them. + ## Logging Postgres queries To enable query logs for other categories of statements: diff --git a/apps/docs/content/troubleshooting/cant-access-supabase-project-lovable-cloud.mdx b/apps/docs/content/troubleshooting/cant-access-supabase-project-lovable-cloud.mdx index ab90657e827..b090af0c2ab 100644 --- a/apps/docs/content/troubleshooting/cant-access-supabase-project-lovable-cloud.mdx +++ b/apps/docs/content/troubleshooting/cant-access-supabase-project-lovable-cloud.mdx @@ -60,8 +60,7 @@ For more information, read [the Lovable Cloud FAQ](https://docs.lovable.dev/feat size="medium" className="text-foreground-light mt-8 mb-6" > -
- @@ -70,10 +69,7 @@ For more information, read [the Lovable Cloud FAQ](https://docs.lovable.dev/feat -
- -
- @@ -82,10 +78,7 @@ For more information, read [the Lovable Cloud FAQ](https://docs.lovable.dev/feat -
- -
- @@ -96,10 +89,7 @@ For more information, read [the Lovable Cloud FAQ](https://docs.lovable.dev/feat -
- -
- @@ -108,10 +98,7 @@ For more information, read [the Lovable Cloud FAQ](https://docs.lovable.dev/feat -
- -
- @@ -122,6 +109,4 @@ For more information, read [the Lovable Cloud FAQ](https://docs.lovable.dev/feat -
- diff --git a/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx b/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx index fa187a3db9d..bddc58bf5f7 100644 --- a/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx +++ b/apps/docs/content/troubleshooting/disabling-prepared-statements-qL8lEL.mdx @@ -13,7 +13,7 @@ database_id = "04801b69-e7eb-4f40-8d41-81110397bbc2" 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: +## Prisma: add ?pgbouncer=true to end of connection string: @@ -21,7 +21,7 @@ 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: +## Drizzle: Add a prepared false flag to the client: @@ -29,7 +29,7 @@ Add a prepared false flag to the client: export const client = postgres(connectionString, { prepare: false }) ``` -# Node Postgres +## Node Postgres [Just omit the "name" value in a query definition](https://node-postgres.com/features/queries#prepared-statements): @@ -41,16 +41,16 @@ const query = { } ``` -# Psycopg +## Psycopg set the [prepare_threshold](https://www.psycopg.org/psycopg3/docs/api/connections.html#psycopg.Connection.prepare_threshold) to `None`. -# asyncpg +## 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`: +## 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 index 7fa689324a8..252b274381a 100644 --- 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 @@ -9,7 +9,7 @@ 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: +## 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](/dashboard/project/_/logs/explorer). These logs are managed through [Logflare](/blog/supabase-logs-self-hosted) and can be queried with a subset of BigQuery SQL syntax. @@ -45,9 +45,9 @@ 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](/dashboard/project/_/logs/explorer) -## Request object +### Request object -### Cloudflare geographic data: +#### Cloudflare geographic data: **Suggested use cases:** @@ -79,7 +79,7 @@ cross join unnest(request) AS request; cross join unnest(cf) AS cf; ``` -### IP and browser/environment data: +#### IP and browser/environment data: **Suggested use cases:** @@ -107,7 +107,7 @@ cross join unnest(request) AS request; cross join unnest(headers) AS headers; ``` -### Query type and formatting data: +#### Query type and formatting data: **Suggested use cases:** @@ -137,9 +137,9 @@ cross join unnest(request) AS request; cross join unnest(sb) AS sb; ``` -## Response object +### Response object -### Status code: +#### Status code: **Suggested use cases:** @@ -162,9 +162,9 @@ from cross join unnest(response) as response; ``` -# Finding errors +## Finding errors -### API level errors +#### API level errors The `metadata.request.url` contains PostgREST formatted queries. @@ -213,7 +213,7 @@ where 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 +#### Database-level errors However, some errors that are reported through the Database API occur at the Postgres 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. @@ -248,11 +248,11 @@ limit 100; Like PostgREST, Postgres has a [reference table](https://www.postgresql.org/docs/current/errcodes-appendix.html) for interpreting error codes. -## PostgREST server and Cloudflare errors +### 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](/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: +## Practical examples: **Find All Errors:** diff --git a/apps/docs/content/troubleshooting/edge-function-401-error-response.mdx b/apps/docs/content/troubleshooting/edge-function-401-error-response.mdx index 91d669e2415..b6e3ffc3eee 100644 --- a/apps/docs/content/troubleshooting/edge-function-401-error-response.mdx +++ b/apps/docs/content/troubleshooting/edge-function-401-error-response.mdx @@ -143,8 +143,7 @@ Your project uses the [new asymmetric keys](/blog/jwt-signing-keys) for authenti size="medium" className="text-foreground-light mt-8 mb-6" > -
- @@ -154,10 +153,7 @@ In the [Functions Dashboard](/dashboard/project/_/functions/), open the affected ![image](/docs/img/troubleshooting/401_edge_functions_toggle_off_JWT_check.png) - -
-
- @@ -170,9 +166,7 @@ supabase functions deploy YOUR_FUNCTION_NAME --no-verify-jwt -
-
- @@ -192,7 +186,6 @@ curl 'https://api.supabase.com/v1/projects/PROJECT_ID/functions/FUNCTION_NAME' \ -
### Invalid key diff --git a/apps/docs/content/troubleshooting/edge-function-546-error-response.mdx b/apps/docs/content/troubleshooting/edge-function-546-error-response.mdx index 5a632cc7455..cc869fa405a 100644 --- a/apps/docs/content/troubleshooting/edge-function-546-error-response.mdx +++ b/apps/docs/content/troubleshooting/edge-function-546-error-response.mdx @@ -141,7 +141,6 @@ There are a few other queries that may be useful for identifying patterns around size="medium" className="text-foreground-light mt-8 mb-6" > -
-
-
-
- -
-
## Step 3: Correcting the error 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 index 42a667aaeb8..e1c69e3ee29 100644 --- 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 @@ -12,7 +12,7 @@ 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: +## 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](/docs/guides/platform/compute-add-ons#disk-io): @@ -30,7 +30,7 @@ Each compute instance has a default direct connection and pooler connection sett | 12XL | 500 | 9,000 | | 16XL | 500 | 12,000 | -## Configuring direct connections limits +### Configuring direct connections limits > Note: the Supavisor connection limits are hard-coded and cannot be changed without upgrading the compute size: @@ -50,21 +50,21 @@ Then you could run the following SQL in the SQL Editor to see if the changes wen SHOW max_connections; ``` -# Dangers of increasing the direct connection limits +## 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: +#### 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 +#### 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 +##### Each direct connection is a running process that will consume active memory This is a Grafana Chart of unhealthy memory usage: @@ -92,7 +92,7 @@ select ) || ' * ' || current_setting('maintenance_work_mem') || ')) / ' || current_setting('work_mem'); ``` -### CPU +#### 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 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 index 18f13fae035..811abe5dc63 100644 --- 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 @@ -8,11 +8,11 @@ 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 +## 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 +### Table of contents - Querying Logs - `postgres_logs` Table Structure @@ -33,7 +33,7 @@ Logs provide insights into Postgres operations. They help meet compliance requir - Frequently Asked Questions - Other resources -## Querying logs +### Querying logs The most practical way to explore and filter logs is through the [Logs Explorer](/dashboard/project/_/logs/explorer). @@ -47,7 +47,7 @@ Although there are many strategies to filter logs, such as `like` and `in` state The `postgres_logs` table contains Postgres events. -### `postgres_logs` table structure +#### `postgres_logs` table structure The table contains 3 fundamental columns: @@ -73,9 +73,9 @@ cross join unnest(metadata) AS metadata cross join unnest(parsed) AS parsed; ``` -### Parsed metadata fields +#### Parsed metadata fields -#### Query information +##### Query information | Field | Description | Example | | --------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------- | @@ -88,7 +88,7 @@ cross join unnest(parsed) AS parsed; - Identifying slow queries - Identifying failing queries -#### Error/Warning information +##### Error/Warning information | Field | Description | Example | | --------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | @@ -103,7 +103,7 @@ cross join unnest(parsed) AS parsed; - Filter by error severity or SQL code - Get hints, details, and context about error events -#### Connection/Identification information +##### Connection/Identification information | Field | Description | Example | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | @@ -124,9 +124,9 @@ cross join unnest(parsed) AS parsed; - Filter connections by sessions for debugging - identify extension events -## Filtering logs +### Filtering logs -### Excluding routine events +#### 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). @@ -139,7 +139,7 @@ where not regexp_contains(event_message, '^cron|PgBouncer|checkpoint|connection received|authenticated|authorized'); ``` -### By timeframe +#### By timeframe To investigate issues around a specific period: @@ -150,7 +150,7 @@ where timestamp between '2024-05-06 04:44:00' and '2024-05-06 04:45:00' ``` -### By error severity +#### By error severity This filter finds all errors, fatals, and panics: @@ -169,7 +169,7 @@ where 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 +#### By query > NOTE: Unless pg_audit is configured, only failed queries are logged @@ -187,11 +187,11 @@ Queries can use complex syntax, so it is often helpful to isolate by referenced - `^`: look for values at start of string - `|`: or operator -## By APIs/roles +### 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 +##### Server/Role mapping API servers have assigned database roles for connecting to the database: @@ -217,7 +217,7 @@ where ... ``` -## By Dashboard queries +### 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. @@ -228,7 +228,7 @@ where regexp_contains(parsed.query, '-- source: dashboard') ``` -## Full example for finding errors +### Full example for finding errors ```sql select @@ -255,9 +255,9 @@ order by timestamp desc limit 100; ``` -# Logging for compliance and security +## Logging for compliance and security -### Customized object and role activity logging +#### Customized object and role activity logging > ⚠️ NOTE: This is specifically designated for those using the `postgres` role or [custom roles](/docs/guides/database/postgres/roles) to interact with their database. Those using the Database REST API should reference the [Database API Logging Guide](https://github.com/orgs/supabase/discussions/22849) instead. @@ -279,7 +279,7 @@ where parsed.user_name = 'API_role' ``` -### Filtering by IP +#### Filtering by IP > If you are connecting from a known, limited range of IP addresses, you should enable [network restrictions](/docs/guides/platform/network-restrictions). @@ -306,7 +306,7 @@ order by ip_count desc limit 100; ``` -# Reviewing log settings +## Reviewing log settings The `pg_settings` table describes system and logging configurations. @@ -337,11 +337,11 @@ where and name like '%log%'; ``` -## Changing log settings +### Changing log settings > WARNING: lenient settings can lead to over-logging, impacting database performance while creating noise in the logs. -#### Severity levels +##### 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). @@ -365,7 +365,7 @@ alter role postgres set log_min_messages = ''; show log_min_messages; -- default WARNING ``` -#### Configuring queries logged +##### Configuring queries logged By default, only failed queries are logged. The [PGAudit extension](/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: @@ -374,25 +374,25 @@ By default, only failed queries are logged. The [PGAudit extension](/docs/guides - database object - entire database -#### Logging within database functions +##### Logging within database functions To track or debug functions, logging can be configured by following the [function debugging guide](/docs/guides/database/functions#general-logging) -# Frequently Asked Questions +## Frequently Asked Questions -#### How to join different log tables +##### How to join different log tables No, log tables are independent from each other and do not share any primary/foreign key relations for joining. -#### How to download logs +##### How to download logs At the moment, the way to download logs is through the Log Dashboard as a CSV -#### What is logged? +##### 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: +#### 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) diff --git a/apps/docs/content/troubleshooting/identify-lovable-cloud-or-supabase-backend.mdx b/apps/docs/content/troubleshooting/identify-lovable-cloud-or-supabase-backend.mdx index 681ba694b81..ad7d7d68cc0 100644 --- a/apps/docs/content/troubleshooting/identify-lovable-cloud-or-supabase-backend.mdx +++ b/apps/docs/content/troubleshooting/identify-lovable-cloud-or-supabase-backend.mdx @@ -60,8 +60,7 @@ If the page displays the Supabase icon, your Supabase project name, and some lin size="medium" className="text-foreground-light mt-8 mb-6" > -
- @@ -70,10 +69,7 @@ If the page displays the Supabase icon, your Supabase project name, and some lin -
- -
- @@ -82,10 +78,7 @@ If the page displays the Supabase icon, your Supabase project name, and some lin -
- -
- @@ -96,7 +89,6 @@ If the page displays the Supabase icon, your Supabase project name, and some lin -
## Lovable Cloud – specific questions @@ -109,8 +101,7 @@ If the page displays the Supabase icon, your Supabase project name, and some lin size="medium" className="text-foreground-light mt-8 mb-6" > -
- @@ -123,10 +114,7 @@ If the page displays the Supabase icon, your Supabase project name, and some lin -
- -
- @@ -137,5 +125,4 @@ If the page displays the Supabase icon, your Supabase project name, and some lin -
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 index 7d49046e4a0..940c7afc7d4 100644 --- a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx +++ b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx @@ -9,7 +9,7 @@ database_id = "ef05da0a-f8bc-44a4-9719-5ae811dba104" > [Guide](/docs/guides/monitoring-troubleshooting/metrics#deploying-supabase-grafana) for setting up Supabase Grafana -# CPU +## CPU Here are examples of unhealthy CPU utilization: @@ -25,13 +25,13 @@ The CPU chart shows 4 distinct metrics of interest: 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: +#### 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: +#### Optimizing 1. [Optimize your queries](/docs/guides/database/query-optimization). 2. [Add indexes](https://github.com/orgs/supabase/discussions/22449) if possible. diff --git a/apps/docs/content/troubleshooting/kong-stops-responding-under-heavy-load-locally.mdx b/apps/docs/content/troubleshooting/kong-stops-responding-under-heavy-load-locally.mdx new file mode 100644 index 00000000000..cb08ae9aae5 --- /dev/null +++ b/apps/docs/content/troubleshooting/kong-stops-responding-under-heavy-load-locally.mdx @@ -0,0 +1,62 @@ +--- +title = "Kong stops responding under heavy load in local development" +topics = [ "cli", "self-hosting", "storage" ] +keywords = [ "kong", "local", "workers", "nginx", "worker_processes", "storage", "concurrent", "timeout", "socket" ] + +[api] +cli = [ "supabase-start" ] +--- + +When running Supabase locally with the CLI, the Kong API gateway can stop +responding under heavy load. This typically happens when many parallel +requests are made (for example, bulk operations against the Storage API): +Kong starts terminating socket connections and logs errors about not having +enough available workers. + +## Why this happens + +To keep the local stack lightweight, the CLI starts Kong with a single nginx +worker process (`KONG_NGINX_WORKER_PROCESSES=1`). A single worker minimizes +memory usage across the ~12 containers that make up the local stack, but it +also limits how many concurrent connections Kong can handle. When the number +of in-flight requests exceeds what one worker can serve, Kong becomes +unresponsive and drops connections. + +## How to fix it + +You can override the number of Kong nginx worker processes by setting the +`KONG_NGINX_WORKER_PROCESSES` environment variable before starting the local +stack. Set it to a specific number, or to `auto` to let Kong allocate one +worker per available CPU: + +```bash +# Use one worker per CPU core +KONG_NGINX_WORKER_PROCESSES=auto supabase start + +# Or pick a fixed number of workers +KONG_NGINX_WORKER_PROCESSES=2 supabase start +``` + +You can also export the variable so it applies to every command in your shell +session: + +```bash +export KONG_NGINX_WORKER_PROCESSES=auto +supabase start +``` + +Increasing the worker count lets Kong handle more parallel connections at the +cost of higher memory usage. If you don't set the variable, the CLI keeps the +default of `1` worker to minimize the local stack's memory footprint. + +After changing the value, restart the stack for it to take effect: + +```bash +supabase stop +KONG_NGINX_WORKER_PROCESSES=auto supabase start +``` + +## Additional resources + +- [Local development guide](/docs/guides/cli/local-development) +- [CLI repository](https://github.com/supabase/cli) diff --git a/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx b/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx index 7c027809550..5269a1661dc 100644 --- a/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx +++ b/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx @@ -9,7 +9,7 @@ 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 +## Cannot create/edit/delete cron jobs Cron jobs can only be modified with the respective SQL functions: @@ -23,13 +23,13 @@ If you are trying to make changes, use the cron functions. If the cron functions --- -# Cron Jobs are not running +## Cron Jobs are not running > You should consider initiating a software upgrade in the [Infrastructure Settings](/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: +### Debugging steps: -### Check to see if "pg_cron scheduler" is active +#### 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 @@ -56,7 +56,7 @@ If the query does not return a row, the worker has died. To revive it, you must
-### Check the `cron.job_run_details` table for more information +#### 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 : @@ -77,7 +77,7 @@ Respond to the errors exposed appropriately.
-### Check if there are too many cron jobs running concurrently +#### 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. @@ -110,7 +110,7 @@ You can view your concurrent peak connection usage throughout the day at the bot
-### Check for database strain +#### Check for database strain Unfortunately, excessive resource strain can slow down or disrupt jobs. @@ -125,7 +125,7 @@ It is important to make sure you are running the latest release of pg_cron (1.6.
-### Check the log explorer for more information +#### 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](/dashboard/project/*/logs/explorer) for failure events with the following query @@ -156,7 +156,7 @@ If you're interested in modifying the query, there is an advanced [guide](https:
-### Create custom logs within cron jobs +#### 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: @@ -188,12 +188,12 @@ You can then search for your custom messages in the [Logs Interface](/dashboard/
-### Upgrading pg_cron version +#### 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](/dashboard/project/*/settings/infrastructure) to get the latest extension.
-### Contacting support and the maintainers +#### 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/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx b/apps/docs/content/troubleshooting/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx index 09c1c7314bf..27d6f72b96a 100644 --- a/apps/docs/content/troubleshooting/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx +++ b/apps/docs/content/troubleshooting/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx @@ -6,7 +6,6 @@ keywords = [ "postgrest", "column does not exist" ] [[errors]] http_status_code = 400 message = "column example_table.example_column does not exist" - --- If you receive a `400` error with the message `column example_table.example_column does not exist` only on mutation requests (`PATCH`, `POST`, `DELETE`) — while `SELECT` queries on the same column work fine — this is a known bug in PostgREST versions before 14.4 ([issue #3707](https://github.com/PostgREST/postgrest/issues/3707)). diff --git a/apps/docs/content/troubleshooting/prisma-error-management-Cm5P_o.mdx b/apps/docs/content/troubleshooting/prisma-error-management-Cm5P_o.mdx index 17f76095870..430d7d11e0c 100644 --- a/apps/docs/content/troubleshooting/prisma-error-management-Cm5P_o.mdx +++ b/apps/docs/content/troubleshooting/prisma-error-management-Cm5P_o.mdx @@ -27,7 +27,7 @@ message = "Drift detected: Your database schema is not in sync with your migrati > This guide has been deprecated. Use the troubleshooting guide in the [Supabase docs](/docs/guides/database/prisma/prisma-troubleshooting). -# Addressing specific errors: +## Addressing specific errors Prisma, unlike other libraries, uses [query parameters for configurations](https://www.prisma.io/docs/orm/overview/databases/postgresql#arguments). @@ -37,7 +37,7 @@ Some can be used to address specific errors and can be appended to end of your c .../postgres?KEY1=VALUE&KEY2=VALUE&KEY3=VALUE ``` -## `Can't reach database server at`: +### Can't reach database server Increase `connect_timeout` to 30s and check to make sure you are using a valid connection string. @@ -45,7 +45,7 @@ Increase `connect_timeout` to 30s and check to make sure you are using a valid c .../postgres?connect_timeout=30 ``` -## `Timed out fetching a new connection from the connection pool`: +### Timed out fetching a new connection from the connection pool Increase `pool_timeout` to 30s . @@ -53,7 +53,7 @@ Increase `pool_timeout` to 30s . .../postgres?pool_timeout=30 ``` -## `... prepared statement "" already exists` +### Prepared statement already exists Add pgbouncer=true to the connection string. @@ -61,23 +61,23 @@ Add pgbouncer=true to the connection string. .../postgres?pgbouncer=true ``` -## `Max client connections reached` +### Max client connections reached Check out this [guide](https://github.com/orgs/supabase/discussions/22305) for managing this error -## `Server has closed the connection` +### 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` +### 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 +## Management suggestions -## Make a custom role for Prisma to increase observability +### Make a custom role for Prisma to increase observability **Imagine your database as a house, and users as the people with keys.** @@ -85,7 +85,7 @@ Some users have discussed how they managed this problem in a [GitHub Discussion. - 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](/docs/guides/database/extensions/pgaudit?queryGroups=database-method&database-method=sql) and [pg_stat_activity](/docs/guides/platform/performance). -### Creating the Prisma user: +#### Creating the Prisma user ```sql create user "prisma" with password 'secret_password' bypassrls createdb; @@ -93,7 +93,7 @@ create user "prisma" with password 'secret_password' bypassrls createdb; > Prisma requires the [`createdb` modifier](/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: +#### Give Postgres ownership of the new user This allows you to view Prisma migration changes in the [Dashboard](/dashboard/project/_/editor) @@ -101,7 +101,7 @@ This allows you to view Prisma migration changes in the [Dashboard](/dashboard/p grant "prisma" to "postgres"; ``` -### Keep it safe! +#### Keep it safe! Use a strong password for Prisma. Bitwarden provides a free [password generator](https://bitwarden.com/password-generator/) that can make one for you. @@ -111,7 +111,7 @@ If you need to change it later, you can use the below SQL: alter user "prisma" with password 'new_password'; ``` -### Grant Prisma access +#### Grant Prisma access The below example gives Prisma full authority over all database objects in the public schema: @@ -129,7 +129,7 @@ The below example gives Prisma full authority over all database objects in the p > For more guidance on specifying access, check out this [article](/blog/postgres-roles-and-privileges#creating-objects-and-assigning-privileges) on privileges -## Optimize Prisma queries: +### Optimize Prisma queries In the [Query Performance Advisor](/dashboard/project/_/database/query-performance), you can view long-running or frequently accessed queries by role: @@ -141,7 +141,7 @@ In the [Query Performance Advisor](/dashboard/project/_/database/query-performan Selecting a query can reveal suggestions to improve its performance -## Configuring connections +### Configuring connections Useful Links: @@ -150,7 +150,7 @@ Useful Links: Supabase provides 3 database connection strings that can be used simultaneously if necessary. You can find them on the dashboard by clicking [Connect](/dashboard/project/_?showConnect=true). -### Direct connection: +#### 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](/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). @@ -160,7 +160,7 @@ Best used with stationary servers, such as VMs and long-standing containers, but postgresql://postgres:[PASSWORD]@db.[PROJECT REF].supabase.co:5432/postgres ``` -### Supavisor in session mode (port 5432): +#### Supavisor in session mode (port 5432) ```md # Example Connection @@ -172,7 +172,7 @@ 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): +#### Supavisor in transaction mode (port 6543) ```md # Example Connection 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 index 264992e271f..b9b532be608 100644 --- a/apps/docs/content/troubleshooting/resolving-500-status-authentication-errors-7bU5U8.mdx +++ b/apps/docs/content/troubleshooting/resolving-500-status-authentication-errors-7bU5U8.mdx @@ -10,19 +10,19 @@ database_id = "031ba6d7-4928-4d95-a2da-bff8dbb740ec" http_status_code = 500 --- -# Resolving 500 status authentication errors +## 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 +#### Prerequisites -#### Open the log explorer +##### Open the log explorer Ensure you have access to the [Dashboard's Log Explorer](/dashboard/project/_/logs/explorer) and set the time range appropriately: ![image](/docs/img/troubleshooting/152d65ad-f0ed-47cf-8dcb-1e31c6221e71.png) -#### Improving log readability +##### 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: @@ -32,9 +32,9 @@ Logs are displayed in a table format, which can be challenging to read. Double-c src="https://github.com/user-attachments/assets/6f4c833c-ae15-41e1-9aa8-48ebd58741a1" /> -## Section 1: Checking for database-level errors +### Section 1: Checking for database-level errors -### Query for recent database 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: @@ -62,15 +62,15 @@ limit 100; If no results are returned, proceed to Section 2. -### Common database-level errors +#### Common database-level errors There are few known categories of auth/database level errors: -### Constraint related (sql_state_code = 23503 or 23\*) +#### 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 +##### Solution The log will show the name of the constraint. You need `DROP` it: @@ -90,11 +90,11 @@ ALTER TABLE ADD CONSTRAINT FOREIGN KEY ( OWNER TO supabase Alternatively, you can run the SQL script in this [GitHub Gist](https://gist.github.com/TheOtherBrian1/4714a333432b80660ff71b136b298fb8) to change all -### Trigger related: +#### 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: @@ -116,7 +116,7 @@ DROP FUNCTION () CASCADE; -- DROP TRIGGER on auth.; ``` -#### Solutions: +##### Solutions: Get the function's definition with this query: @@ -126,11 +126,11 @@ from pg_proc where proname = ''; ``` -##### Trigger has insufficient privileges ( sql_state_code = 42501) +###### 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](/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) +###### 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: @@ -139,13 +139,13 @@ The trigger may be referencing a table or column that no longer exists. In that - remove the trigger - recreate the database object that the trigger referenced -### Corrupted schema +#### 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 +### Section 2: Checking Auth level errors -### Query for Auth errors +#### Query for Auth errors Run this SQL query in the Log Explorer to find Auth-related errors: @@ -167,7 +167,7 @@ where order by timestamp ``` -### Database migration errors +#### Database migration errors > `running db migrations: Migrator: problem creating schema migrations` @@ -175,7 +175,7 @@ This is a continuation of the "Corrupted Schema" error from the Postgres Section 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, contact Support. -### SMTP errors +#### 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: @@ -186,7 +186,7 @@ The logs may contain messages about `gomail`. It means that auth is struggling t 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 +### 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. diff --git a/apps/docs/content/troubleshooting/restore-project-after-90-days-pause.mdx b/apps/docs/content/troubleshooting/restore-project-after-90-days-pause.mdx index 69ff401808e..3c91adbfe70 100644 --- a/apps/docs/content/troubleshooting/restore-project-after-90-days-pause.mdx +++ b/apps/docs/content/troubleshooting/restore-project-after-90-days-pause.mdx @@ -81,8 +81,7 @@ chmod +x sync_supabase_config.sh size="medium" className="text-foreground-light mt-8 mb-6" > -
- @@ -280,7 +279,6 @@ echo "Done. Configs saved to ${OUTDIR}/" -
The script saves both source and target configs to a local `config_sync_/` directory so you can review exactly what changed. Use `--dry-run` to preview differences without applying them. 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 index edabf7e92c2..d159b35512b 100644 --- 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 @@ -10,13 +10,13 @@ database_id = "fb1cbd42-e172-44b2-af2b-fda5aecde5c2" cli = [ "supabase-inspect-db" ] --- -# Optimizing your database +## Optimizing your database This is an intermediate and actionable guide for Postgres optimization within the Supabase ecosystem. > Consider checking out [Index_advisor](/docs/guides/database/extensions/index_advisor) and the [performance advisor](/dashboard/project/_/database/performance-advisor) now available in the Dashboard! -## Installing Supabase Grafana +### 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 [Observability Dashboard](/dashboard/project/_/observability) 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. @@ -25,7 +25,7 @@ _Visual of Grafana Dashboard_ 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](/docs/guides/telemetry/metrics/grafana-self-hosted) -## Query optimization through indexes +### 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. diff --git a/apps/docs/content/troubleshooting/storage-error-403-forbidden-new-row-violates-row-level-security-policy-on-upload-a94384.mdx b/apps/docs/content/troubleshooting/storage-error-403-forbidden-new-row-violates-row-level-security-policy-on-upload-a94384.mdx new file mode 100644 index 00000000000..614436439de --- /dev/null +++ b/apps/docs/content/troubleshooting/storage-error-403-forbidden-new-row-violates-row-level-security-policy-on-upload-a94384.mdx @@ -0,0 +1,22 @@ +--- +title = "Storage error: 403 Forbidden: 'new row violates row-level security policy' on upload" +date_created = "2026-06-19T09:05:11+00:00" +topics = [ "auth", "database", "storage" ] +keywords = [] +[[errors]] +http_status_code = 403 +message = "Forbidden" + +--- + +If you are observing a 403 Forbidden error with the message 'new row violates row-level security policy' when uploading files, it typically indicates that the database cannot return the metadata for the newly created object. This can happen even if your INSERT policies are correctly defined and the user's JWT is valid. + +**Why Does This Happen?** +The Supabase Storage API executes an `INSERT` operation followed by a `RETURNING *` clause to provide object details back to the client. If a **SELECT** RLS policy is missing or does not cover the object being uploaded, the database is unable to return the row metadata. This results in a policy violation that causes the entire transaction to fail. + +**How to Resolve:** +Add a **SELECT** RLS policy to the `example_schema.example_table` (specifically `storage.objects`) that mirrors your `INSERT` requirements. Ensure the policy allows the authenticated user to read the record they are currently creating. + +- For example, if your INSERT policy is restricted to `auth.uid()`, your SELECT policy must also permit access based on `auth.uid()` or the specific bucket and path. + +You can manage your RLS policies via the [Dashboard](/dashboard/project/_/auth/policies) or the [SQL editor](/dashboard/project/_/sql/new). 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 index 538446d3c95..35751edb9fc 100644 --- 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 @@ -7,18 +7,18 @@ keywords = [ "ipv4", "ipv6", "network", "compatibility", "address" ] database_id = "f27145c7-0ff5-4621-a364-5d5704bce0ff" --- -# Network compatibility with your Supabase database +## 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: +#### Supabase and IPv6 All Supabase databases provide a direct connection string that maps to an IPv6 address. -### Working with IPv6 incompatible hosts: +#### Working with IPv6 incompatible hosts Here are your options if your server platform doesn't support IPv6: @@ -28,7 +28,7 @@ Here are your options if your server platform doesn't support IPv6: > Note: the IPv4 Add-On costs an hour, which equates to ~ if left on for a full month (~720 hours) -### Checking IPv6 support: +#### Checking IPv6 support The majority of services are IPv6 compatible. However, there are a few prominent ones that only accept IPv4 connections: @@ -45,7 +45,7 @@ curl -6 https://ifconfig.co/ip If the command returns an IPv6 address, the network is IPv6 compatible. -### Finding your database's IP address: +#### 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: @@ -57,7 +57,7 @@ This command queries the domain name servers to find the IP address of the given Example IPv6 Address: `2a05:d014:1c06:5f0c:d7a9:8616:bee2:30df` -### Identifying your connections: +#### Identifying your connections The pooler and direct connection strings can be found on the dashboard by clicking [Connect](/dashboard/project/_?showConnect=true). @@ -68,14 +68,14 @@ The pooler and direct connection strings can be found on the dashboard by clicki postgresql://postgres:[YOUR-PASSWORD]@db.ajrbwkcuthywfihaarmflo.supabase.co:5432/postgres ``` -#### Supavisor in transaction mode (port 6543) +##### 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) +##### Supavisor in session mode (port 5432) ```sh # Example session string diff --git a/apps/docs/data/errorCodes/authErrorCodes.json b/apps/docs/data/errorCodes/authErrorCodes.json new file mode 100644 index 00000000000..4363902d411 --- /dev/null +++ b/apps/docs/data/errorCodes/authErrorCodes.json @@ -0,0 +1,293 @@ +{ + "anonymous_provider_disabled": { + "description": "Anonymous sign-ins are disabled." + }, + "bad_code_verifier": { + "description": "Returned from the PKCE flow where the provided code verifier does not match the expected one. Indicates a bug in the implementation of the client library." + }, + "bad_json": { + "description": "Usually used when the HTTP body of the request is not valid JSON." + }, + "bad_jwt": { + "description": "JWT sent in the Authorization header is not valid." + }, + "bad_oauth_callback": { + "description": "OAuth callback from provider to Auth does not have all the required attributes (state). Indicates an issue with the OAuth provider or client library implementation." + }, + "bad_oauth_state": { + "description": "OAuth state (data echoed back by the OAuth provider to Supabase Auth) is not in the correct format. Indicates an issue with the OAuth provider integration." + }, + "captcha_failed": { + "description": "CAPTCHA challenge could not be verified with the CAPTCHA provider. Check your CAPTCHA integration." + }, + "conflict": { + "description": "General database conflict, such as concurrent requests on resources that should not be modified concurrently. Can often occur when you have too many session refresh requests firing off at the same time for a user. Check your app for concurrency issues, and if detected, back off exponentially." + }, + "email_address_invalid": { + "description": "Example and test domains are currently not supported. Use a different email address." + }, + "email_address_not_authorized": { + "description": "Email sending is not allowed for this address as your project is using the default SMTP service. Emails can only be sent to members in your Supabase organization. If you want to send emails to others, set up a custom SMTP provider.", + "references": [ + { + "href": "https://supabase.com/docs/guides/auth/auth-smtp", + "description": "Setting up a custom SMTP provider" + } + ] + }, + "email_conflict_identity_not_deletable": { + "description": "Unlinking this identity causes the user's account to change to an email address which is already used by another user account. Indicates an issue where the user has two different accounts using different primary email addresses. You may need to migrate user data to one of their accounts in this case." + }, + "email_exists": { + "description": "Email address already exists in the system." + }, + "email_not_confirmed": { + "description": "Signing in is not allowed for this user as the email address is not confirmed." + }, + "email_provider_disabled": { + "description": "Signups are disabled for email and password." + }, + "flow_state_expired": { + "description": "PKCE flow state to which the API request relates has expired. Ask the user to sign in again." + }, + "flow_state_not_found": { + "description": "PKCE flow state to which the API request relates no longer exists. Flow states expire after a while and are progressively cleaned up, which can cause this error. Retried requests can cause this error, as the previous request likely destroyed the flow state. Ask the user to sign in again." + }, + "hook_payload_invalid_content_type": { + "description": "Payload from Auth does not have a valid Content-Type header." + }, + "hook_payload_over_size_limit": { + "description": "Payload from Auth exceeds maximum size limit." + }, + "hook_timeout": { + "description": "Unable to reach hook within maximum time allocated." + }, + "hook_timeout_after_retry": { + "description": "Unable to reach hook after maximum number of retries." + }, + "identity_already_exists": { + "description": "The identity to which the API relates is already linked to a user." + }, + "identity_not_found": { + "description": "Identity to which the API call relates does not exist, such as when an identity is unlinked or deleted." + }, + "insufficient_aal": { + "description": "To call this API, the user must have a higher Authenticator Assurance Level. To resolve, ask the user to solve an MFA challenge.", + "references": [ + { + "href": "https://supabase.com/docs/guides/auth/auth-mfa", + "description": "MFA" + } + ] + }, + "invite_not_found": { + "description": "Invite is expired or already used." + }, + "invalid_credentials": { + "description": "Login credentials or grant type not recognized." + }, + "manual_linking_disabled": { + "description": "Calling the supabase.auth.linkUser() and related APIs is not enabled on the Auth server." + }, + "mfa_challenge_expired": { + "description": "Responding to an MFA challenge should happen within a fixed time period. Request a new challenge when encountering this error." + }, + "mfa_factor_name_conflict": { + "description": "MFA factors for a single user should not have the same friendly name." + }, + "mfa_factor_not_found": { + "description": "MFA factor no longer exists." + }, + "mfa_ip_address_mismatch": { + "description": "The enrollment process for MFA factors must begin and end with the same IP address." + }, + "mfa_phone_enroll_not_enabled": { + "description": "Enrollment of MFA Phone factors is disabled." + }, + "mfa_phone_verify_not_enabled": { + "description": "Login via Phone factors and verification of new Phone factors is disabled." + }, + "mfa_totp_enroll_not_enabled": { + "description": "Enrollment of MFA TOTP factors is disabled." + }, + "mfa_totp_verify_not_enabled": { + "description": "Login via TOTP factors and verification of new TOTP factors is disabled." + }, + "mfa_verification_failed": { + "description": "MFA challenge could not be verified -- wrong TOTP code." + }, + "mfa_verification_rejected": { + "description": "Further MFA verification is rejected. Only returned if the MFA verification attempt hook returns a reject decision.", + "references": [ + { + "href": "https://supabase.com/docs/guides/auth/auth-hooks/mfa-verification-hook", + "description": "MFA verification hook" + } + ] + }, + "mfa_verified_factor_exists": { + "description": "Verified phone factor already exists for a user. Unenroll existing verified phone factor to continue." + }, + "mfa_web_authn_enroll_not_enabled": { + "description": "Enrollment of MFA Web Authn factors is disabled." + }, + "mfa_web_authn_verify_not_enabled": { + "description": "Login via WebAuthn factors and verification of new WebAuthn factors is disabled." + }, + "no_authorization": { + "description": "This HTTP request requires an Authorization header, which is not provided." + }, + "not_admin": { + "description": "User accessing the API is not admin, i.e. the JWT does not contain a role claim that identifies them as an admin of the Auth server." + }, + "oauth_provider_not_supported": { + "description": "Using an OAuth provider which is disabled on the Auth server." + }, + "otp_disabled": { + "description": "Sign in with OTPs (magic link, email OTP) is disabled. Check your server's configuration." + }, + "otp_expired": { + "description": "OTP code for this sign-in has expired. Ask the user to sign in again." + }, + "over_email_send_rate_limit": { + "description": "Too many emails have been sent to this email address. Ask the user to wait a while before trying again." + }, + "over_request_rate_limit": { + "description": "Too many requests have been sent by this client (IP address). Ask the user to try again in a few minutes. Sometimes can indicate a bug in your application that mistakenly sends out too many requests (such as a badly written useEffect React hook).", + "references": [ + { + "href": "https://react.dev/reference/react/useEffect", + "description": "React useEffect hook" + } + ] + }, + "over_sms_send_rate_limit": { + "description": "Too many SMS messages have been sent to this phone number. Ask the user to wait a while before trying again." + }, + "phone_exists": { + "description": "Phone number already exists in the system." + }, + "phone_not_confirmed": { + "description": "Signing in is not allowed for this user as the phone number is not confirmed." + }, + "phone_provider_disabled": { + "description": "Signups are disabled for phone and password." + }, + "provider_disabled": { + "description": "OAuth provider is disabled for use. Check your server's configuration." + }, + "provider_email_needs_verification": { + "description": "Not all OAuth providers verify their user's email address. Supabase Auth requires emails to be verified, so this error is sent out when a verification email is sent after completing the OAuth flow." + }, + "reauthentication_needed": { + "description": "A user needs to reauthenticate to change their password. Ask the user to reauthenticate by calling the supabase.auth.reauthenticate() API." + }, + "reauthentication_not_valid": { + "description": "Verifying a reauthentication failed, the code is incorrect. Ask the user to enter a new code." + }, + "refresh_token_not_found": { + "description": "Session containing the refresh token not found." + }, + "refresh_token_already_used": { + "description": "Refresh token has been revoked and falls outside the refresh token reuse interval. See the documentation on sessions for further information.", + "references": [ + { + "href": "https://supabase.com/docs/guides/auth/sessions", + "description": "Auth sessions" + } + ] + }, + "request_timeout": { + "description": "Processing the request took too long. Retry the request." + }, + "same_password": { + "description": "A user that is updating their password must use a different password than the one currently used." + }, + "saml_assertion_no_email": { + "description": "SAML assertion (user information) was received after sign in, but no email address was found in it, which is required. Check the provider's attribute mapping and/or configuration." + }, + "saml_assertion_no_user_id": { + "description": "SAML assertion (user information) was received after sign in, but a user ID (called NameID) was not found in it, which is required. Check the SAML identity provider's configuration." + }, + "saml_entity_id_mismatch": { + "description": "(Admin API.) Updating the SAML metadata for a SAML identity provider is not possible, as the entity ID in the update does not match the entity ID in the database. This is equivalent to creating a new identity provider, and you should do that instead." + }, + "saml_idp_already_exists": { + "description": "(Admin API.) Adding a SAML identity provider that is already added." + }, + "saml_idp_not_found": { + "description": "SAML identity provider not found. Most often returned after IdP-initiated sign-in with an unregistered SAML identity provider in Supabase Auth." + }, + "saml_metadata_fetch_failed": { + "description": "(Admin API.) Adding or updating a SAML provider failed as its metadata could not be fetched from the provided URL." + }, + "saml_provider_disabled": { + "description": "Using Enterprise SSO with SAML 2.0 is not enabled on the Auth server.", + "references": [ + { + "href": "https://supabase.com/docs/guides/auth/enterprise-sso/auth-sso-saml", + "description": "Enterprise SSO" + } + ] + }, + "saml_relay_state_expired": { + "description": "SAML relay state is an object that tracks the progress of a supabase.auth.signInWithSSO() request. The SAML identity provider should respond after a fixed amount of time, after which this error is shown. Ask the user to sign in again." + }, + "saml_relay_state_not_found": { + "description": "SAML relay states are progressively cleaned up after they expire, which can cause this error. Ask the user to sign in again." + }, + "session_expired": { + "description": "Session to which the API request relates has expired. This can occur if an inactivity timeout is configured, or the session entry has exceeded the configured timebox value. See the documentation on sessions for more information.", + "references": [ + { + "href": "https://supabase.com/docs/guides/auth/sessions", + "description": "Auth sessions" + } + ] + }, + "session_not_found": { + "description": "Session to which the API request relates no longer exists. This can occur if the user has signed out, or the session entry in the database was deleted in some other way." + }, + "signup_disabled": { + "description": "Sign ups (new account creation) are disabled on the server." + }, + "single_identity_not_deletable": { + "description": "Every user must have at least one identity attached to it, so deleting (unlinking) an identity is not allowed if it's the only one for the user." + }, + "sms_send_failed": { + "description": "Sending an SMS message failed. Check your SMS provider configuration." + }, + "sso_domain_already_exists": { + "description": "(Admin API.) Only one SSO domain can be registered per SSO identity provider." + }, + "sso_provider_not_found": { + "description": "SSO provider not found. Check the arguments in supabase.auth.signInWithSSO()." + }, + "too_many_enrolled_mfa_factors": { + "description": "A user can only have a fixed number of enrolled MFA factors." + }, + "unexpected_audience": { + "description": "(Deprecated feature not available via Supabase client libraries.) The request's X-JWT-AUD claim does not match the JWT's audience." + }, + "unexpected_failure": { + "description": "Auth service is degraded or a bug is present, without a specific reason." + }, + "user_already_exists": { + "description": "User with this information (email address, phone number) cannot be created again as it already exists." + }, + "user_banned": { + "description": "User to which the API request relates has a banned_until property which is still active. No further API requests should be attempted until this field is cleared." + }, + "user_not_found": { + "description": "User to which the API request relates no longer exists." + }, + "user_sso_managed": { + "description": "When a user comes from SSO, certain fields of the user cannot be updated (like email)." + }, + "validation_failed": { + "description": "Provided parameters are not in the expected format." + }, + "weak_password": { + "description": "User is signing up or changing their password without meeting the password strength criteria. Use the AuthWeakPasswordError class to access more information about what they need to do to make the password pass." + } +} diff --git a/apps/docs/data/errorCodes/realtimeErrorCodes.json b/apps/docs/data/errorCodes/realtimeErrorCodes.json new file mode 100644 index 00000000000..6b8f910648f --- /dev/null +++ b/apps/docs/data/errorCodes/realtimeErrorCodes.json @@ -0,0 +1,213 @@ +{ + "TopicNameRequired": { + "description": "You are trying to use Realtime without a topic name set." + }, + "RealtimeDisabledForConfiguration": { + "description": "The configuration provided to Realtime on connect will not be able to provide you any Postgres Changes.", + "resolution": "Verify your configuration on channel startup as you might not have your tables properly registered." + }, + "TenantNotFound": { + "description": "The tenant you are trying to connect to does not exist.", + "resolution": "Verify the tenant name you are trying to connect to exists in the realtime.tenants table." + }, + "ErrorConnectingToWebsocket": { + "description": "Error when trying to connect to the WebSocket server.", + "resolution": "Verify user information on connect." + }, + "ErrorAuthorizingWebsocket": { + "description": "Error when trying to authorize the WebSocket connection.", + "resolution": "Verify user information on connect." + }, + "TableHasSpacesInName": { + "description": "The table you are trying to listen to has spaces in its name which we are unable to support.", + "resolution": "Change the table name to not have spaces in it." + }, + "UnableToDeleteTenant": { + "description": "Error when trying to delete a tenant." + }, + "UnableToSetPolicies": { + "description": "Error when setting up Authorization Policies." + }, + "UnableCheckoutConnection": { + "description": "Error when trying to checkout a connection from the tenant pool." + }, + "UnableToSubscribeToPostgres": { + "description": "Error when trying to subscribe to Postgres changes." + }, + "ReconnectSubscribeToPostgres": { + "description": "Postgres changes still waiting to be subscribed." + }, + "ChannelRateLimitReached": { + "description": "The number of channels you can create has reached its limit." + }, + "ConnectionRateLimitReached": { + "description": "The number of connected clients has reached its limit." + }, + "ClientJoinRateLimitReached": { + "description": "The rate of joins per second from your clients has reached the channel limits." + }, + "RealtimeDisabledForTenant": { + "description": "Realtime has been disabled for the tenant.", + "resolution": "Your project may have been suspended for exceeding usage quotas. Contact support with your project reference ID and a description of your Realtime use case.", + "references": [ + { + "href": "https://supabase.com/docs/troubleshooting/realtime-project-suspended-for-exceeding-quotas", + "description": "Troubleshooting guide for suspended projects" + } + ] + }, + "UnableToConnectToTenantDatabase": { + "description": "Realtime was not able to connect to the tenant's database." + }, + "DatabaseLackOfConnections": { + "description": "Realtime was not able to connect to the tenant's database due to not having enough available connections.", + "resolution": "Verify your database connection limits.", + "references": [ + { + "href": "https://supabase.com/docs/guides/database/connection-management", + "description": "Connection management guide" + } + ] + }, + "RealtimeNodeDisconnected": { + "description": "Realtime is a distributed application and this means that one the system is unable to communicate with one of the distributed nodes." + }, + "MigrationsFailedToRun": { + "description": "Error when running the migrations against the Tenant database that are required by Realtime." + }, + "StartListenAndReplicationFailed": { + "description": "Error when starting the replication and listening of errors for database broadcasting." + }, + "ReplicationMaxWalSendersReached": { + "description": "Maximum number of WAL senders reached in tenant database.", + "references": [ + { + "href": "https://supabase.com/docs/guides/database/custom-postgres-config#cli-configurable-settings", + "description": "Configuring max WAL senders" + } + ] + }, + "MigrationCheckFailed": { + "description": "Check to see if we require to run migrations fails." + }, + "PartitionCreationFailed": { + "description": "Error when creating partitions for realtime.messages." + }, + "ErrorStartingPostgresCDCStream": { + "description": "Error when starting the Postgres CDC stream which is used for Postgres Changes." + }, + "UnknownDataProcessed": { + "description": "An unknown data type was processed by the Realtime system." + }, + "ErrorStartingPostgresCDC": { + "description": "Error when starting the Postgres CDC extension which is used for Postgres Changes." + }, + "ReplicationSlotBeingUsed": { + "description": "The replication slot is being used by another transaction." + }, + "PoolingReplicationPreparationError": { + "description": "Error when preparing the replication slot." + }, + "PoolingReplicationError": { + "description": "Error when pooling the replication slot." + }, + "SubscriptionDeletionFailed": { + "description": "Error when trying to delete a subscription for postgres changes." + }, + "UnableToDeletePhantomSubscriptions": { + "description": "Error when trying to delete subscriptions that are no longer being used." + }, + "UnableToCheckProcessesOnRemoteNode": { + "description": "Error when trying to check the processes on a remote node." + }, + "UnableToCreateCounter": { + "description": "Error when trying to create a counter to track rate limits for a tenant." + }, + "UnableToIncrementCounter": { + "description": "Error when trying to increment a counter to track rate limits for a tenant." + }, + "UnableToDecrementCounter": { + "description": "Error when trying to decrement a counter to track rate limits for a tenant." + }, + "UnableToUpdateCounter": { + "description": "Error when trying to update a counter to track rate limits for a tenant." + }, + "UnableToFindCounter": { + "description": "Error when trying to find a counter to track rate limits for a tenant." + }, + "UnhandledProcessMessage": { + "description": "Unhandled message received by a Realtime process." + }, + "UnableToTrackPresence": { + "description": "Error when handling track presence for this socket." + }, + "UnknownPresenceEvent": { + "description": "Presence event type not recognized by service." + }, + "IncreaseConnectionPool": { + "description": "The number of connections you have set for Realtime are not enough to handle your current use case." + }, + "RlsPolicyError": { + "description": "Error on RLS policy used for authorization." + }, + "ConnectionInitializing": { + "description": "Database is initializing connection." + }, + "DatabaseConnectionIssue": { + "description": "Database had connection issues and connection was not able to be established." + }, + "UnableToConnectToProject": { + "description": "Unable to connect to Project database." + }, + "InvalidJWTExpiration": { + "description": "JWT exp claim value it's incorrect." + }, + "JwtSignatureError": { + "description": "JWT signature was not able to be validated." + }, + "MalformedJWT": { + "description": "Token received does not comply with the JWT format." + }, + "Unauthorized": { + "description": "Unauthorized access to Realtime channel." + }, + "RealtimeRestarting": { + "description": "Realtime is currently restarting." + }, + "UnableToProcessListenPayload": { + "description": "Payload sent in NOTIFY operation was not JSON parsable." + }, + "UnableToListenToTenantDatabase": { + "description": "Unable to LISTEN for notifications against the Tenant Database." + }, + "UnprocessableEntity": { + "description": "Received a HTTP request with a body that was not able to be processed by the endpoint." + }, + "InitializingProjectConnection": { + "description": "Connection against Tenant database is still starting." + }, + "TimeoutOnRpcCall": { + "description": "RPC request within the Realtime server has timed out." + }, + "ErrorOnRpcCall": { + "description": "Error when calling another realtime node." + }, + "ErrorExecutingTransaction": { + "description": "Error executing a database transaction in tenant database." + }, + "SynInitializationError": { + "description": "Our framework to syncronize processes has failed to properly startup a connection to the database." + }, + "JanitorFailedToDeleteOldMessages": { + "description": "Scheduled task for realtime.message cleanup was unable to run." + }, + "UnableToEncodeJson": { + "description": "An error were we are not handling correctly the response to be sent to the end user." + }, + "UnknownErrorOnController": { + "description": "An error we are not handling correctly was triggered on a controller." + }, + "UnknownErrorOnChannel": { + "description": "An error we are not handling correctly was triggered on a channel." + } +} diff --git a/apps/docs/features/docs/Troubleshooting.utils.common.mjs b/apps/docs/features/docs/Troubleshooting.utils.common.mjs index c938dced206..de3e1028e48 100644 --- a/apps/docs/features/docs/Troubleshooting.utils.common.mjs +++ b/apps/docs/features/docs/Troubleshooting.utils.common.mjs @@ -61,6 +61,7 @@ export const TroubleshootingSchema = z z.enum([ 'ai', 'ai-tools', + 'api', 'auth', 'branching', 'cli', @@ -129,12 +130,10 @@ export async function getAllTroubleshootingEntriesInternal() { const parseResult = validateTroubleshootingMetadata(frontmatter) if ('error' in parseResult) { - console.error( - `Error validating troubleshooting metadata\nEntry:%O\nError:%O`, - frontmatter, - parseResult.error + throw Error( + `Error validating troubleshooting metadata for ${filePath}`, + { cause: parseResult.error } ) - return null } const mdxTree = fromMarkdown(content, { diff --git a/apps/docs/features/ui/ErrorCodes.tsx b/apps/docs/features/ui/ErrorCodes.tsx index ea9fa98022a..f003d78eab3 100644 --- a/apps/docs/features/ui/ErrorCodes.tsx +++ b/apps/docs/features/ui/ErrorCodes.tsx @@ -1,8 +1,8 @@ +import _authErrorCodes from '~/data/errorCodes/authErrorCodes.json' +import _realtimeErrorCodes from '~/data/errorCodes/realtimeErrorCodes.json' +import { type ErrorCodeDefinition } from '~/resources/error/errorTypes' import Link from 'next/link' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui' -import _authErrorCodes from '~/content/errorCodes/authErrorCodes.toml' -import _realtimeErrorCodes from '~/content/errorCodes/realtimeErrorCodes.toml' -import { type ErrorCodeDefinition } from '~/resources/error/errorTypes' const errorCodesByService = { auth: _authErrorCodes as Record, diff --git a/apps/docs/internals/generate-guides-markdown.ts b/apps/docs/internals/generate-guides-markdown.ts index 22aee30a6ae..39abdcba099 100644 --- a/apps/docs/internals/generate-guides-markdown.ts +++ b/apps/docs/internals/generate-guides-markdown.ts @@ -11,14 +11,21 @@ import { toMarkdown } from 'mdast-util-to-markdown' import { gfm } from 'micromark-extension-gfm' import { mdxjs } from 'micromark-extension-mdxjs' -import { getInternalLinkBaseUrl, prefixInternalLinks } from './internal-links' +import { addBaseUrlPrefix } from './internal-links' import { Admonition } from './markdown-schema/Admonition' +import { AuthProviders } from './markdown-schema/AuthProviders' +import { ComputeDiskLimitsTable } from './markdown-schema/ComputeDiskLimitsTable' +import { ErrorCodes } from './markdown-schema/ErrorCodes' import { Link } from './markdown-schema/Link' import { MetricsStackCards } from './markdown-schema/MetricsStackCards' +import { NavData } from './markdown-schema/NavData' import { Panel } from './markdown-schema/Panel' +import { Price } from './markdown-schema/Price' +import { RealtimeLimitsEstimator } from './markdown-schema/RealtimeLimitsEstimator' +import { RegionsList, SmartRegionsList } from './markdown-schema/RegionsList' +import { SharedData } from './markdown-schema/SharedData' import { StepHike } from './markdown-schema/StepHike' import { TabPanel } from './markdown-schema/TabPanel' -import { Price } from './markdown-schema/Price' const PARTIALS_DIR = path.join(process.cwd(), 'content', '_partials') @@ -131,23 +138,32 @@ function applySchema(parent: Parent, schema: ComponentSchema): void { */ const SCHEMA: ComponentSchema = { Admonition, + AuthProviders, + ComputeDiskLimitsTable, + ErrorCodes, Link, Price, GlassPanel: Panel, IconPanel: Panel, + RealtimeLimitsEstimator, + RegionsList, + SmartRegionsList, ...StepHike, TabPanel, MetricsStackCards, + NavData, + SharedData, } -async function generateOne(filePath: string, linkBaseUrl: string): Promise { +async function generateOne(filePath: string): Promise { const raw = await fs.readFile(filePath, 'utf8') const { content, data } = matter(raw) const tree = parseMdx(content) await inlinePartials(tree) + addBaseUrlPrefix(tree) applySchema(tree, SCHEMA) - const body = prefixInternalLinks(serializeMdx(tree), linkBaseUrl) + const body = serializeMdx(tree) const headerParts: string[] = [] if (data.title) headerParts.push(`# ${data.title}`) @@ -163,7 +179,6 @@ async function generateOne(filePath: string, linkBaseUrl: string): Promise( path.join(process.cwd(), 'content/reference/javascript/v2/typeSpec.json') ) - const linkBaseUrl = getInternalLinkBaseUrl() await Promise.all( REFERENCES.map(async (ref) => { @@ -407,7 +410,17 @@ async function generate() { output = await renderCli(ref) break } - await fs.writeFile(path.join(OUT_DIR, ref.outFile), prefixInternalLinks(output, linkBaseUrl)) + const tree = fromMarkdown(output, { + extensions: [gfm()], + mdastExtensions: [gfmFromMarkdown()], + }) + addBaseUrlPrefix(tree) + const prefixed = toMarkdown(tree, { + extensions: [gfmToMarkdown()], + bullet: '-', + listItemIndent: 'one', + }) + await fs.writeFile(path.join(OUT_DIR, ref.outFile), prefixed) }) ) diff --git a/apps/docs/internals/internal-links.test.ts b/apps/docs/internals/internal-links.test.ts index 54a4bf783e5..d2bc19999d9 100644 --- a/apps/docs/internals/internal-links.test.ts +++ b/apps/docs/internals/internal-links.test.ts @@ -1,6 +1,7 @@ +import { fromMarkdown } from 'mdast-util-from-markdown' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { getInternalLinkBaseUrl, prefixInternalLinks, withDocsBasePath } from './internal-links' +import { addBaseUrlPrefix, getInternalLinkBaseUrl, withDocsBasePath } from './internal-links' describe('withDocsBasePath', () => { it('prepends /docs to a root-relative href', () => { @@ -95,147 +96,46 @@ describe('getInternalLinkBaseUrl', () => { }) }) -describe('prefixInternalLinks', () => { - const BASE = 'https://supabase.com' +describe('addBaseUrlPrefix', () => { + const ORIGINAL_ENV = process.env - it('returns content unchanged when baseUrl is empty', () => { - const input = 'See [Dashboard](/dashboard/foo).' - expect(prefixInternalLinks(input, '')).toBe(input) + beforeEach(() => { + process.env = { ...ORIGINAL_ENV, VERCEL_ENV: 'production' } }) - it('prepends baseUrl to a root-relative link', () => { - expect(prefixInternalLinks('See [Dashboard](/dashboard/foo).', BASE)).toBe( - 'See [Dashboard](https://supabase.com/dashboard/foo).' - ) + afterEach(() => { + process.env = ORIGINAL_ENV }) - it('rewrites multiple links on the same line', () => { - const input = 'A [one](/a) and [two](/b/c) here.' - expect(prefixInternalLinks(input, BASE)).toBe( - 'A [one](https://supabase.com/a) and [two](https://supabase.com/b/c) here.' - ) + const linkUrls = (markdown: string): string[] => { + const tree = fromMarkdown(markdown) + addBaseUrlPrefix(tree) + const urls: string[] = [] + const visit = (n: any) => { + if (n.type === 'link') urls.push(n.url) + if (Array.isArray(n.children)) n.children.forEach(visit) + } + visit(tree) + return urls + } + + it('prepends baseUrl to root-relative link URLs', () => { + expect(linkUrls('[home](/foo)')).toEqual(['https://supabase.com/foo']) }) - it('preserves query strings and fragments', () => { - expect(prefixInternalLinks('[link](/foo?bar=1&baz=2#section)', BASE)).toBe( - '[link](https://supabase.com/foo?bar=1&baz=2#section)' - ) + it('leaves absolute, anchor, and protocol-relative URLs alone', () => { + expect(linkUrls('[a](https://x.com) [b](#h) [c](//cdn/x)')).toEqual([ + 'https://x.com', + '#h', + '//cdn/x', + ]) }) - it('leaves absolute http(s) links alone', () => { - const input = 'See [GitHub](https://github.com/supabase).' - expect(prefixInternalLinks(input, BASE)).toBe(input) - }) - - it('leaves anchor-only links alone', () => { - const input = 'Jump to [section](#installation).' - expect(prefixInternalLinks(input, BASE)).toBe(input) - }) - - it('leaves mailto and other schemes alone', () => { - const input = 'Email [us](mailto:team@example.com) or [call](tel:+1234).' - expect(prefixInternalLinks(input, BASE)).toBe(input) - }) - - it('leaves explicitly relative links (./, ../) alone', () => { - const input = 'See [sibling](./sibling) and [parent](../parent).' - expect(prefixInternalLinks(input, BASE)).toBe(input) - }) - - it('leaves protocol-relative (//host) URLs alone', () => { - const input = 'CDN [asset](//cdn.example.com/img.png).' - expect(prefixInternalLinks(input, BASE)).toBe(input) - }) - - it('does not rewrite image syntax', () => { - const input = 'An image: ![alt text](/static/foo.png).' - expect(prefixInternalLinks(input, BASE)).toBe(input) - }) - - it('rewrites a link adjacent to an image without touching the image', () => { - expect(prefixInternalLinks('![logo](/logo.png) and [home](/dashboard)', BASE)).toBe( - '![logo](/logo.png) and [home](https://supabase.com/dashboard)' - ) + it('does not rewrite image URLs', () => { + expect(linkUrls('![alt](/img.png)')).toEqual([]) }) it('skips links inside fenced code blocks', () => { - const input = [ - 'Before: [yes](/touch-me).', - '', - '```md', - '[ignore me](/leave-alone)', - '```', - '', - 'After: [also yes](/touch-me-too).', - ].join('\n') - - expect(prefixInternalLinks(input, BASE)).toBe( - [ - 'Before: [yes](https://supabase.com/touch-me).', - '', - '```md', - '[ignore me](/leave-alone)', - '```', - '', - 'After: [also yes](https://supabase.com/touch-me-too).', - ].join('\n') - ) - }) - - it('handles multiple fenced code blocks correctly', () => { - const input = [ - '[a](/a)', - '```', - '[skip1](/skip1)', - '```', - '[b](/b)', - '```ts', - '[skip2](/skip2)', - '```', - '[c](/c)', - ].join('\n') - - expect(prefixInternalLinks(input, BASE)).toBe( - [ - '[a](https://supabase.com/a)', - '```', - '[skip1](/skip1)', - '```', - '[b](https://supabase.com/b)', - '```ts', - '[skip2](/skip2)', - '```', - '[c](https://supabase.com/c)', - ].join('\n') - ) - }) - - it('rewrites links with empty text', () => { - expect(prefixInternalLinks('[](/foo)', BASE)).toBe('[](https://supabase.com/foo)') - }) - - it('uses any baseUrl passed in, not just supabase.com', () => { - expect(prefixInternalLinks('[x](/y)', 'https://branch-deploy.vercel.app')).toBe( - '[x](https://branch-deploy.vercel.app/y)' - ) - }) - - it('is a no-op when there are no matching links', () => { - const input = '# Title\n\nJust prose, no links.' - expect(prefixInternalLinks(input, BASE)).toBe(input) - }) - - it('handles an unclosed code fence by leaving the unclosed portion untouched', () => { - // A `split(/(```...```)/)` only pairs complete fences; an unclosed fence - // means everything after it stays in the trailing prose segment. Document - // that behavior rather than promising to parse malformed markdown. - const input = ['[before](/before)', '```', '[inside-unclosed](/inside)'].join('\n') - expect(prefixInternalLinks(input, BASE)).toBe( - [ - '[before](https://supabase.com/before)', - '```', - '[inside-unclosed](https://supabase.com/inside)', - ].join('\n') - ) + expect(linkUrls('```\n[x](/x)\n```\n\n[y](/y)')).toEqual(['https://supabase.com/y']) }) }) diff --git a/apps/docs/internals/internal-links.ts b/apps/docs/internals/internal-links.ts index ae19af20595..54bb5d5ac19 100644 --- a/apps/docs/internals/internal-links.ts +++ b/apps/docs/internals/internal-links.ts @@ -1,3 +1,6 @@ +import { Root } from 'mdast' +import { visit } from 'unist-util-visit' + const DOCS_BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/docs' /** @@ -34,24 +37,14 @@ export function getInternalLinkBaseUrl(): string { return '' } -/** - * Rewrite root-relative markdown links by prepending `baseUrl`: - * `[text](/foo)` → `[text](${baseUrl}/foo)` - * - * Skips fenced code blocks, image syntax (`![alt](...)`), protocol-relative - * URLs (`//host/...`), and non-root-relative targets (`http://`, `mailto:`, - * `#anchor`, `./`, `../`). - */ -export function prefixInternalLinks(content: string, baseUrl: string): string { - if (!baseUrl) return content - const segments = content.split(/(```[\s\S]*?```)/g) - return segments - .map((seg, i) => { - if (i % 2 === 1) return seg - return seg.replace(/(? { - if (url.startsWith('//')) return match - return `${text}(${baseUrl}${url})` - }) - }) - .join('') +export function addBaseUrlPrefix(tree: Root) { + const baseUrl = getInternalLinkBaseUrl() + + visit(tree, 'link', (node) => { + if (node.url.startsWith('/') && !node.url.startsWith('//')) { + node.url = baseUrl + node.url + } + }) + + return tree } diff --git a/apps/docs/internals/markdown-schema/AuthProviders.ts b/apps/docs/internals/markdown-schema/AuthProviders.ts new file mode 100644 index 00000000000..09359583edb --- /dev/null +++ b/apps/docs/internals/markdown-schema/AuthProviders.ts @@ -0,0 +1,10 @@ +import authProviders from '../../data/authProviders' +import { withDocsBasePath } from '../internal-links' + +export const AuthProviders = ({ props }: { props: Record }): string => { + const type = String(props.type ?? '') + return authProviders + .filter((p) => p.authType === type) + .map((p) => `- [${p.name}](${withDocsBasePath(p.href)})`) + .join('\n') +} diff --git a/apps/docs/internals/markdown-schema/ComputeDiskLimitsTable.ts b/apps/docs/internals/markdown-schema/ComputeDiskLimitsTable.ts new file mode 100644 index 00000000000..cb6101ca2f5 --- /dev/null +++ b/apps/docs/internals/markdown-schema/ComputeDiskLimitsTable.ts @@ -0,0 +1,29 @@ +import { createRequire } from 'node:module' + +// tsx's ESM loader can't pick up named exports from the `shared-data` package +// (CJS, no `"type": "module"`). Load via `createRequire` for CJS interop — +// this file only runs in the build script, never in the Next.js bundle. +const { + COMPUTE_BASELINE_IOPS, + COMPUTE_BASELINE_THROUGHPUT, + COMPUTE_DISK, + COMPUTE_MAX_IOPS, + COMPUTE_MAX_THROUGHPUT, +} = createRequire(import.meta.url)('shared-data') as { + COMPUTE_BASELINE_IOPS: Record + COMPUTE_BASELINE_THROUGHPUT: Record + COMPUTE_DISK: Record + COMPUTE_MAX_IOPS: Record + COMPUTE_MAX_THROUGHPUT: Record +} + +export const ComputeDiskLimitsTable = (): string => ` +| Compute Instance | Baseline Throughput (MB/s) | Max Throughput (MB/s) | Baseline IOPS | Max IOPS | +| --- | --- | --- | --- | --- | +${Object.entries(COMPUTE_DISK) + .map( + ([key, value]) => + `| ${value.name} | ${COMPUTE_BASELINE_THROUGHPUT[key]?.toLocaleString()} MB/s | ${COMPUTE_MAX_THROUGHPUT[key]?.toLocaleString()} MB/s | ${COMPUTE_BASELINE_IOPS[key]?.toLocaleString()} IOPS | ${COMPUTE_MAX_IOPS[key]?.toLocaleString()} IOPS |` + ) + .join('\n')} +` diff --git a/apps/docs/internals/markdown-schema/ErrorCodes.ts b/apps/docs/internals/markdown-schema/ErrorCodes.ts new file mode 100644 index 00000000000..a765a15ef8e --- /dev/null +++ b/apps/docs/internals/markdown-schema/ErrorCodes.ts @@ -0,0 +1,42 @@ +import authErrorCodes from '../../data/errorCodes/authErrorCodes.json' +import realtimeErrorCodes from '../../data/errorCodes/realtimeErrorCodes.json' +import { type ErrorCodeDefinition } from '../../resources/error/errorTypes' + +const errorCodesByService: Record> = { + auth: authErrorCodes as Record, + realtime: realtimeErrorCodes as Record, +} + +// Pipes break the surrounding table layout, so escape any that appear in cell text. +const escapeCell = (value: string): string => value.replace(/\|/g, '\\|') + +export const ErrorCodes = ({ props }: { props: Record }): string => { + const service = String(props.service ?? '') + const errorCodes = errorCodesByService[service] + if (!errorCodes) return '' + + const entries = Object.entries(errorCodes).sort(([aCode], [bCode]) => aCode.localeCompare(bCode)) + const hasResolutions = entries.some(([, definition]) => definition.resolution) + + const headings = ['Error code', 'Description', ...(hasResolutions ? ['Action'] : [])] + const headerRow = `| ${headings.join(' | ')} |` + const dividerRow = `| ${headings.map(() => '---').join(' | ')} |` + + const rows = entries.map(([code, definition]) => { + let description = escapeCell(definition.description) + if (definition.references?.length) { + const links = definition.references + .map((reference) => `[${escapeCell(reference.description)}](${reference.href})`) + .join(', ') + description += ` Learn more: ${links}` + } + + const cells = [`\`${code}\``, description] + if (hasResolutions) { + cells.push(definition.resolution ? escapeCell(definition.resolution) : '') + } + return `| ${cells.join(' | ')} |` + }) + + return [headerRow, dividerRow, ...rows].join('\n') +} diff --git a/apps/docs/internals/markdown-schema/NavData.ts b/apps/docs/internals/markdown-schema/NavData.ts new file mode 100644 index 00000000000..487c75d6b7e --- /dev/null +++ b/apps/docs/internals/markdown-schema/NavData.ts @@ -0,0 +1,16 @@ +import { navDataForMdx } from '../../components/Navigation/NavigationMenu/NavigationMenu.constants' +import { withDocsBasePath } from '../internal-links' + +type NavItem = { name?: string; url?: string } + +export const NavData = ({ props }: { props: Record }): string => { + const dataset = navDataForMdx[props.data as keyof typeof navDataForMdx] + if (!dataset) return '' + + // Datasets are either a flat array of items or a section with an `items` list. + const items: NavItem[] = Array.isArray(dataset) ? dataset : (dataset.items ?? []) + return items + .filter((item) => item.url) + .map((item) => `- [${item.name}](${withDocsBasePath(String(item.url))})`) + .join('\n') +} diff --git a/apps/docs/internals/markdown-schema/RealtimeLimitsEstimator.ts b/apps/docs/internals/markdown-schema/RealtimeLimitsEstimator.ts new file mode 100644 index 00000000000..7d07d8dc8a4 --- /dev/null +++ b/apps/docs/internals/markdown-schema/RealtimeLimitsEstimator.ts @@ -0,0 +1,36 @@ +import { + COMPUTE_LABELS, + THROUGHPUT_TABLE_HEADINGS, +} from '../../components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.constants' +import throughputTable from '../../data/realtime/throughput.json' + +type Row = { + computeAddOn: string + filters: boolean + rls: boolean + concurrency: number + maxDBChanges: number + maxMessagesPerClient: number + totalMessagesPerSecond: number + p95Latency: number +} + +const headerRow = `| ${THROUGHPUT_TABLE_HEADINGS.join(' | ')} |` +const dividerRow = `| ${THROUGHPUT_TABLE_HEADINGS.map(() => '---').join(' | ')} |` + +const renderGroup = (computeAddOn: string): string => { + const rows = (throughputTable as Row[]).filter((l) => l.computeAddOn === computeAddOn) + return `#### ${COMPUTE_LABELS[computeAddOn] ?? computeAddOn} + +${headerRow} +${dividerRow} +${rows + .map( + (l) => + `| ${l.filters ? 'Yes' : 'No'} | ${l.rls ? 'Yes' : 'No'} | ${l.concurrency.toLocaleString()} | ${l.maxDBChanges} | ${l.maxMessagesPerClient} | ${l.totalMessagesPerSecond.toLocaleString()} | ${l.p95Latency}ms |` + ) + .join('\n')}` +} + +export const RealtimeLimitsEstimator = (): string => + [...new Set((throughputTable as Row[]).map((l) => l.computeAddOn))].map(renderGroup).join('\n\n') diff --git a/apps/docs/internals/markdown-schema/RegionsList.ts b/apps/docs/internals/markdown-schema/RegionsList.ts new file mode 100644 index 00000000000..3ff0496c965 --- /dev/null +++ b/apps/docs/internals/markdown-schema/RegionsList.ts @@ -0,0 +1,22 @@ +import { createRequire } from 'node:module' + +// tsx's ESM loader can't pick up named exports from the `shared-data` package +// (CJS, no `"type": "module"`). Load via `createRequire` for CJS interop — +// this file only runs in the build script, never in the Next.js bundle. +const req = createRequire(import.meta.url) +const { AWS_REGIONS } = req('shared-data') as { + AWS_REGIONS: Record +} +const { SMART_REGION_TO_EXACT_REGION_MAP } = req('shared-data/regions') as { + SMART_REGION_TO_EXACT_REGION_MAP: Map +} + +export const RegionsList = (): string => + Object.values(AWS_REGIONS) + .map((r) => `- ${r.displayName}, \`${r.code}\``) + .join('\n') + +export const SmartRegionsList = (): string => + [...SMART_REGION_TO_EXACT_REGION_MAP.entries()] + .map(([smart, exact]) => `- ${smart}, \`${exact}\``) + .join('\n') diff --git a/apps/docs/internals/markdown-schema/SharedData.ts b/apps/docs/internals/markdown-schema/SharedData.ts new file mode 100644 index 00000000000..c915f95d7c2 --- /dev/null +++ b/apps/docs/internals/markdown-schema/SharedData.ts @@ -0,0 +1,46 @@ +import { createRequire } from 'node:module' + +import { resolveSharedDataPath } from '../../components/SharedData.utils' + +// tsx's ESM loader can't pick up named exports from the `shared-data` package +// (CJS, no `"type": "module"`). Load via `createRequire` to use CJS interop — +// this file only runs in the build script, never in the Next.js bundle. +const { config, logConstants } = createRequire(import.meta.url)('shared-data') + +type Field = { path: string; type: string } +type Schema = { name: string; fields: Field[] } + +const sharedData: Record = { config, logConstants } + +const renderLogConstants = (data: { schemas: Schema[] }): string => + data.schemas + .map( + (s) => + `#### ${s.name}\n${[...s.fields] + .sort((a, b) => a.path.localeCompare(b.path)) + .map((f) => ` - \`${f.path}\`, \`${f.type}\``) + .join('\n')}` + ) + .join('\n\n') + +export const SharedData = ({ + props, + children, +}: { + props: Record + children: string +}): string => { + const dataset = sharedData[String(props.data ?? '')] + if (!dataset) return children + + // String-path pattern: `a.b.c`. + const value = resolveSharedDataPath(dataset, children.trim()) + if (value != null) return String(value) + + // Render-function pattern: `{(d) => …}`. + // The schema walker strips the MDX expression children before this handler + // runs, and we can't evaluate the function statically anyway — hardcode the + // markdown for the only dataset that uses this form today. + if (props.data === 'logConstants') return renderLogConstants(dataset as { schemas: Schema[] }) + return '' +} diff --git a/apps/docs/package.json b/apps/docs/package.json index bf086e3a985..2dd0afcbd89 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -62,7 +62,7 @@ "@octokit/plugin-retry": "^8.1.0", "@sentry/nextjs": "catalog:", "@supabase/supabase-js": "catalog:", - "@tanstack/react-query": "^5.13.4", + "@tanstack/react-query": "~5.83.0", "acorn": "^8.11.3", "acorn-typescript": "^1.4.13", "ai-commands": "workspace:*", diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 503ffcb45c4..6e9c430ab9a 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -130,6 +130,7 @@ Jared Patterson Jason Farber Jean-Paul Argudo Jeff Smick +Jefferson Venerando Jenny Kibiri Jeremias Menichelli Jess Fink @@ -189,6 +190,7 @@ Manan Gupta Marcus Weiner Marija Milicevic Mark Burggraf +Márton Boros Matthew Hambright Matt Hudson Matt Johnston diff --git a/apps/docs/public/img/guides/auth-mfa/auth-mfa-flow.svg b/apps/docs/public/img/guides/auth-mfa/auth-mfa-flow.svg deleted file mode 100644 index d7dd78a4bf0..00000000000 --- a/apps/docs/public/img/guides/auth-mfa/auth-mfa-flow.svg +++ /dev/null @@ -1 +0,0 @@ -
Yes
No
1 or more factors
0 factors
Setup flow
Session is AAL1
Enroll API
Show QR code
User: Scan QR code in authenticator
User: Enter code
Challenge + Verify API
Is code correct?
Upgrade to AAL2
Done
Login flow
User: Sign-in
Upgrade to AAL1
List Factors API
User: Open authenticator
Setup flow
\ No newline at end of file diff --git a/apps/docs/public/img/guides/auth-mfa/auth-mfa-phone-flow.svg b/apps/docs/public/img/guides/auth-mfa/auth-mfa-phone-flow.svg deleted file mode 100644 index 976dd3525c4..00000000000 --- a/apps/docs/public/img/guides/auth-mfa/auth-mfa-phone-flow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
Yes
No
1 or more factors
0 factors
Setup flow
Session is AAL1
Enroll API
Challenge API
Code sent to User
User: Enter code
Verify API
Is code correct?
Upgrade to AAL2
Done
Login flow
User: Sign-in
Upgrade to AAL1
List Factors API
User: Select phone factor
Setup flow
\ No newline at end of file diff --git a/apps/docs/public/img/guides/auth-signing-keys/states.svg b/apps/docs/public/img/guides/auth-signing-keys/states.svg deleted file mode 100644 index fc49db026d7..00000000000 --- a/apps/docs/public/img/guides/auth-signing-keys/states.svg +++ /dev/null @@ -1,2 +0,0 @@ - -

A new key is created and advertized

Once all components have picked up the new key, new JWTs can be issued with it

Rotation, JWT remain accepted

Once all JWTs created with the previous key expire (or sooner)

Delete permanently after 7 days

standby

in_use

previously_used

revoked

diff --git a/apps/docs/public/img/guides/cli/snaplet-example-schema.png b/apps/docs/public/img/guides/cli/snaplet-example-schema.png deleted file mode 100644 index dab45e505a4..00000000000 Binary files a/apps/docs/public/img/guides/cli/snaplet-example-schema.png and /dev/null differ diff --git a/apps/docs/public/img/guides/cli/workflow.png b/apps/docs/public/img/guides/cli/workflow.png deleted file mode 100644 index f23f1c21905..00000000000 Binary files a/apps/docs/public/img/guides/cli/workflow.png and /dev/null differ diff --git a/apps/docs/public/img/guides/database/connecting-to-postgres/connection-decision-tree-light.svg b/apps/docs/public/img/guides/database/connecting-to-postgres/connection-decision-tree-light.svg deleted file mode 100644 index 51bfe3a7a42..00000000000 --- a/apps/docs/public/img/guides/database/connecting-to-postgres/connection-decision-tree-light.svg +++ /dev/null @@ -1 +0,0 @@ -

Where are you connecting from?

Persistent Backend

Serverless / Edge

IPv6 Supported?
IPv4 Add-on?

IPv4 Needed?

Use Direct Connection

Use Supavisor Session Mode

IPv6 Supported?
IPv4 Add-on?

IPv4 Needed?

Use Dedicated Pooler PgBouncer_Pro

Use Supavisor Transaction Mode

\ No newline at end of file diff --git a/apps/docs/public/img/guides/database/connecting-to-postgres/connection-decision-tree.svg b/apps/docs/public/img/guides/database/connecting-to-postgres/connection-decision-tree.svg deleted file mode 100644 index 576e76ea87d..00000000000 --- a/apps/docs/public/img/guides/database/connecting-to-postgres/connection-decision-tree.svg +++ /dev/null @@ -1,102 +0,0 @@ -

Where are you connecting from?

Persistent Backend

Serverless / Edge

IPv6 Supported?
IPv4 Add-on?

IPv4 Needed?

Use Direct Connection

Use Supavisor Session Mode

IPv6 Supported?
IPv4 Add-on?

IPv4 Needed?

Use Dedicated Pooler PgBouncer_Pro

Use Supavisor Transaction Mode

\ No newline at end of file diff --git a/apps/docs/public/img/guides/integrations/onesignal/diagram.png b/apps/docs/public/img/guides/integrations/onesignal/diagram.png deleted file mode 100644 index 77b83ca5315..00000000000 Binary files a/apps/docs/public/img/guides/integrations/onesignal/diagram.png and /dev/null differ diff --git a/apps/docs/public/img/guides/integrations/zuplo/arch.png b/apps/docs/public/img/guides/integrations/zuplo/arch.png deleted file mode 100644 index af425e632a2..00000000000 Binary files a/apps/docs/public/img/guides/integrations/zuplo/arch.png and /dev/null differ diff --git a/apps/docs/public/img/guides/platform/read-replicas/read-replicas-flow.svg b/apps/docs/public/img/guides/platform/read-replicas/read-replicas-flow.svg deleted file mode 100644 index a49c06eb103..00000000000 --- a/apps/docs/public/img/guides/platform/read-replicas/read-replicas-flow.svg +++ /dev/null @@ -1 +0,0 @@ -

No

Yes

No

Yes

No

Yes

Yes

No

Yes

No

Database slowing down

CPU above 70% sustained?

Monitor, do not scale yet

Queries optimized? Indexes in place?

Run EXPLAIN ANALYZE
Add missing indexes
Optimize first

Workload 80%+ reads?

Upgrade compute
Replicas will not help writes

Already at 16XL?

Read Replicas
Only horizontal option left

Need workload isolation
or geo-distribution?

Read Replicas

Either works
Compute is simpler
Replicas scale further

\ No newline at end of file diff --git a/apps/docs/public/img/guides/realtime/realtime-arch.png b/apps/docs/public/img/guides/realtime/realtime-arch.png deleted file mode 100644 index fca216142b1..00000000000 Binary files a/apps/docs/public/img/guides/realtime/realtime-arch.png and /dev/null differ diff --git a/apps/docs/public/img/queues-quickstart-create-dark.png b/apps/docs/public/img/queues-quickstart-create-dark.png new file mode 100644 index 00000000000..e09d45733b8 Binary files /dev/null and b/apps/docs/public/img/queues-quickstart-create-dark.png differ diff --git a/apps/docs/public/img/queues-quickstart-create-light.png b/apps/docs/public/img/queues-quickstart-create-light.png new file mode 100644 index 00000000000..6315c6683aa Binary files /dev/null and b/apps/docs/public/img/queues-quickstart-create-light.png differ diff --git a/apps/docs/public/img/queues-quickstart-create.png b/apps/docs/public/img/queues-quickstart-create.png deleted file mode 100644 index 30ed87e8856..00000000000 Binary files a/apps/docs/public/img/queues-quickstart-create.png and /dev/null differ diff --git a/apps/docs/public/img/queues-quickstart-install-dark.png b/apps/docs/public/img/queues-quickstart-install-dark.png new file mode 100644 index 00000000000..84790fb3389 Binary files /dev/null and b/apps/docs/public/img/queues-quickstart-install-dark.png differ diff --git a/apps/docs/public/img/queues-quickstart-install-light.png b/apps/docs/public/img/queues-quickstart-install-light.png new file mode 100644 index 00000000000..0bf6b74a6d8 Binary files /dev/null and b/apps/docs/public/img/queues-quickstart-install-light.png differ diff --git a/apps/docs/public/img/queues-quickstart-install.png b/apps/docs/public/img/queues-quickstart-install.png deleted file mode 100644 index 6ca44f07e1c..00000000000 Binary files a/apps/docs/public/img/queues-quickstart-install.png and /dev/null differ diff --git a/apps/docs/public/img/queues-quickstart-queue-settings-dark.png b/apps/docs/public/img/queues-quickstart-queue-settings-dark.png new file mode 100644 index 00000000000..d44968addb5 Binary files /dev/null and b/apps/docs/public/img/queues-quickstart-queue-settings-dark.png differ diff --git a/apps/docs/public/img/queues-quickstart-queue-settings-light.png b/apps/docs/public/img/queues-quickstart-queue-settings-light.png new file mode 100644 index 00000000000..34b3ee846e4 Binary files /dev/null and b/apps/docs/public/img/queues-quickstart-queue-settings-light.png differ diff --git a/apps/docs/public/img/queues-quickstart-rls.png b/apps/docs/public/img/queues-quickstart-rls.png deleted file mode 100644 index ed112b31b7f..00000000000 Binary files a/apps/docs/public/img/queues-quickstart-rls.png and /dev/null differ diff --git a/apps/docs/public/img/queues-quickstart-roles-light.png b/apps/docs/public/img/queues-quickstart-roles-light.png deleted file mode 100644 index e43792164a9..00000000000 Binary files a/apps/docs/public/img/queues-quickstart-roles-light.png and /dev/null differ diff --git a/apps/docs/public/img/queues-quickstart-roles.png b/apps/docs/public/img/queues-quickstart-roles.png deleted file mode 100644 index ae72156c430..00000000000 Binary files a/apps/docs/public/img/queues-quickstart-roles.png and /dev/null differ diff --git a/apps/docs/public/img/queues-quickstart-settings.png b/apps/docs/public/img/queues-quickstart-settings.png deleted file mode 100644 index a8138e6c5f1..00000000000 Binary files a/apps/docs/public/img/queues-quickstart-settings.png and /dev/null differ diff --git a/apps/docs/public/img/storage/schema-design.png b/apps/docs/public/img/storage/schema-design.png deleted file mode 100644 index 79401732ce2..00000000000 Binary files a/apps/docs/public/img/storage/schema-design.png and /dev/null differ diff --git a/apps/docs/resources/error/errorSync.ts b/apps/docs/resources/error/errorSync.ts index 7ca729de860..68796e62079 100644 --- a/apps/docs/resources/error/errorSync.ts +++ b/apps/docs/resources/error/errorSync.ts @@ -1,12 +1,12 @@ -import { type PostgrestError } from '@supabase/supabase-js' import { readFile } from 'node:fs/promises' import path from 'node:path' import util, { styleText } from 'node:util' -import { parse } from 'smol-toml' +import { type PostgrestError } from '@supabase/supabase-js' + import { Service } from '../../__generated__/graphql' import { extractMessageFromAnyError, MultiError } from '../../app/api/utils' import { Result } from '../../features/helpers.fn' -import { CONTENT_DIRECTORY } from '../../lib/docs' +import { DOCS_DIRECTORY } from '../../lib/docs' import { DatabaseCorrected } from '../../lib/supabase' import { supabaseAdmin } from '../../lib/supabaseAdmin' import { type ErrorCodeDefinition } from './errorTypes' @@ -14,7 +14,7 @@ import { type ErrorCodeDefinition } from './errorTypes' type ErrorCodeUploadParameters = DatabaseCorrected['content']['Functions']['update_error_code']['Args'] -const ERROR_CODES_DIRECTORY = path.join(CONTENT_DIRECTORY, 'errorCodes') +const ERROR_CODES_DIRECTORY = path.join(DOCS_DIRECTORY, 'data', 'errorCodes') async function doFetchErrorCodes( file: string, @@ -29,9 +29,9 @@ async function doFetchErrorCodes( }) ) ) - .flatMap((toml) => + .flatMap((json) => Result.tryCatchSync( - () => parse(toml) as unknown as Record, + () => JSON.parse(json) as Record, (error) => new Error( `Failed to parse error code file ${file}: ${extractMessageFromAnyError(error)}`, @@ -53,8 +53,8 @@ async function doFetchErrorCodes( async function fetchErrorCodes(): Promise, MultiError>> { const arrayOfResults = await Promise.all([ - doFetchErrorCodes('authErrorCodes.toml', Service.Auth), - doFetchErrorCodes('realtimeErrorCodes.toml', Service.Realtime), + doFetchErrorCodes('authErrorCodes.json', Service.Auth), + doFetchErrorCodes('realtimeErrorCodes.json', Service.Realtime), ]) return Result.transposeArray(arrayOfResults).map((result) => result.flat()) } diff --git a/apps/docs/spec/api_v1_openapi.json b/apps/docs/spec/api_v1_openapi.json index 5cbed875278..1af817aed15 100644 --- a/apps/docs/spec/api_v1_openapi.json +++ b/apps/docs/spec/api_v1_openapi.json @@ -683,6 +683,12 @@ "type": "string" } }, + { + "name": "target_flow", + "required": false, + "in": "query", + "schema": { "type": "string" } + }, { "name": "resource", "required": false, @@ -7704,7 +7710,6 @@ "type": { "type": "string", "enum": ["specific"] }, "code": { "type": "string", - "minLength": 1, "description": "Specific region code. The codes supported are not a stable API, and should be retrieved from the /available-regions endpoint.", "enum": [ "us-east-1", @@ -7779,6 +7784,10 @@ "type": "string", "format": "uri", "description": "Template URL used to create the project from the CLI." + }, + "high_availability": { + "type": "boolean", + "description": "[Experimental] Whether to enable high availability for the project." } }, "required": ["db_pass", "name", "organization_slug"], @@ -9033,6 +9042,20 @@ "type": { "type": "string", "enum": ["pg_graphql_introspection_change"] } }, "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["ltree_reindex_required"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["operator_estimator_gate"] } + }, + "required": ["type"] } ] } diff --git a/apps/docs/spec/transforms/api_v1_openapi_deparsed.json b/apps/docs/spec/transforms/api_v1_openapi_deparsed.json index de1ae800bcc..a52a6d0f5ef 100644 --- a/apps/docs/spec/transforms/api_v1_openapi_deparsed.json +++ b/apps/docs/spec/transforms/api_v1_openapi_deparsed.json @@ -1161,7 +1161,6 @@ }, "code": { "type": "string", - "minLength": 1, "description": "Specific region code. The codes supported are not a stable API, and should be retrieved from the /available-regions endpoint.", "enum": [ "us-east-1", @@ -1239,6 +1238,10 @@ "type": "string", "format": "uri", "description": "Template URL used to create the project from the CLI." + }, + "high_availability": { + "type": "boolean", + "description": "[Experimental] Whether to enable high availability for the project." } }, "required": ["db_pass", "name", "organization_slug"], @@ -1830,6 +1833,14 @@ "type": "string" } }, + { + "name": "target_flow", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "resource", "required": false, @@ -8126,6 +8137,26 @@ } }, "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["ltree_reindex_required"] + } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["operator_estimator_gate"] + } + }, + "required": ["type"] } ] } @@ -23393,7 +23424,6 @@ }, "code": { "type": "string", - "minLength": 1, "description": "Specific region code. The codes supported are not a stable API, and should be retrieved from the /available-regions endpoint.", "enum": [ "us-east-1", @@ -23471,6 +23501,10 @@ "type": "string", "format": "uri", "description": "Template URL used to create the project from the CLI." + }, + "high_availability": { + "type": "boolean", + "description": "[Experimental] Whether to enable high availability for the project." } }, "required": ["db_pass", "name", "organization_slug"], @@ -25398,6 +25432,26 @@ } }, "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["ltree_reindex_required"] + } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["operator_estimator_gate"] + } + }, + "required": ["type"] } ] } diff --git a/apps/docs/styles/globals.css b/apps/docs/styles/globals.css index 8e74b564762..47b3026db95 100644 --- a/apps/docs/styles/globals.css +++ b/apps/docs/styles/globals.css @@ -303,13 +303,26 @@ th code { } /* - * Prevents very long code lines (checksums, URLs, etc., from breaking table - * widths + * First column (e.g. variable names): wrap at word boundaries, not mid-token. */ -.prose :where(td) code { +.prose :where(td:first-child) code { + @apply break-words whitespace-normal; +} + +/* + * Other columns: prevents very long code lines (checksums, URLs, etc.) from + * breaking table widths. + */ +.prose :where(td:not(:first-child)) code { @apply break-all; } +.prose table td:first-child, +.prose table th:first-child { + min-width: 12rem; + width: 30%; +} + /* Zoomable image */ [data-rmiz-modal]:focus, diff --git a/apps/studio/.github/eslint-rule-baselines.json b/apps/studio/.github/eslint-rule-baselines.json index 060c8bde59c..7074c1000f7 100644 --- a/apps/studio/.github/eslint-rule-baselines.json +++ b/apps/studio/.github/eslint-rule-baselines.json @@ -1,19 +1,18 @@ { "rules": { - "react-hooks/exhaustive-deps": 174, + "react-hooks/exhaustive-deps": 172, "import/no-anonymous-default-export": 57, "@tanstack/query/exhaustive-deps": 9, - "@typescript-eslint/no-explicit-any": 966, + "@typescript-eslint/no-explicit-any": 952, "no-restricted-imports": 0, - "no-restricted-exports": 240, - "react/no-unstable-nested-components": 42 + "no-restricted-exports": 235, + "react/no-unstable-nested-components": 41 }, "ruleFiles": { "react-hooks/exhaustive-deps": { "components/grid/components/common/BlockKeys.tsx": 2, "components/grid/components/editor/JsonEditor.tsx": 2, "components/grid/components/editor/TextEditor.tsx": 2, - "components/grid/hooks/useTableSort.ts": 1, "components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx": 1, "components/interfaces/Advisors/CreateRuleSheet.tsx": 1, "components/interfaces/App/RouteValidationWrapper.tsx": 2, @@ -36,7 +35,6 @@ "components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx": 1, "components/interfaces/Billing/Payment/PaymentConfirmation.tsx": 1, "components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement.tsx": 1, - "components/interfaces/Connect/DatabaseConnectionString.tsx": 2, "components/interfaces/ConnectSheet/content/steps/mcp/cursor/content.tsx": 1, "components/interfaces/Database/Backups/PITR/TimeInput.tsx": 4, "components/interfaces/Database/Indexes/Indexes.tsx": 2, @@ -132,6 +130,7 @@ "pages/organizations.tsx": 1, "pages/project/[ref]/auth/policies.tsx": 2, "pages/project/[ref]/editor/[id].tsx": 1, + "pages/project/[ref]/integrations/[id]/index.tsx": 1, "pages/project/[ref]/settings/jwt/legacy.tsx": 1, "pages/project/[ref]/sql/examples.tsx": 1, "pages/project/[ref]/sql/templates.tsx": 1, @@ -258,17 +257,14 @@ "components/interfaces/BranchManagement/Branch.Commands.tsx": 1, "components/interfaces/BranchManagement/DatabaseDiffPanel.tsx": 1, "components/interfaces/BranchManagement/ReviewWithAI.tsx": 1, - "components/interfaces/Connect/ConnectTabs.tsx": 1, "components/interfaces/Database/Backups/PITR/PITR.utils.ts": 1, "components/interfaces/Database/Backups/RestoreToNewProject/BackupsList.tsx": 1, "components/interfaces/Database/Functions/CreateFunction/FunctionEditor.tsx": 1, - "components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx": 3, "components/interfaces/Database/Indexes/Indexes.tsx": 1, "components/interfaces/Database/Migrations/Migrations.tsx": 1, "components/interfaces/Database/Privileges/PrivilegesTable.tsx": 1, "components/interfaces/Database/Publications/PublicationsList.tsx": 5, "components/interfaces/Database/Publications/PublicationsTableItem.tsx": 5, - "components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PublicationsComboBox.tsx": 1, "components/interfaces/Database/Replication/DisableExternalReplicationDialog.tsx": 1, "components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx": 1, "components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx": 1, @@ -284,7 +280,6 @@ "components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx": 1, "components/interfaces/Functions/FunctionsNav.tsx": 1, "components/interfaces/Home/ProjectList/ProjectList.tsx": 1, - "components/interfaces/Home/ProjectUsage.tsx": 1, "components/interfaces/Home/ServiceStatus.tsx": 1, "components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx": 1, "components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx": 2, @@ -294,7 +289,6 @@ "components/interfaces/Integrations/Queues/QueuesSettings.tsx": 1, "components/interfaces/Integrations/Queues/SingleQueue/MessageDetailsPanel.tsx": 1, "components/interfaces/Integrations/Queues/SingleQueue/QueueFilters.tsx": 1, - "components/interfaces/Integrations/Queues/SingleQueue/QueueSettings.tsx": 2, "components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx": 1, "components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx": 1, "components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx": 2, @@ -379,7 +373,6 @@ "components/interfaces/Settings/Logs/PreviewFilterPanelWithUniversal.tsx": 2, "components/interfaces/SignIn/SignInForm.tsx": 1, "components/interfaces/SignIn/SignInWithCustom.tsx": 1, - "components/interfaces/SignIn/SignInWithGitHub.tsx": 1, "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableSheet.tsx": 2, "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/InitializeForeignSchemaDialog.tsx": 1, "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx": 4, @@ -443,7 +436,6 @@ "components/layouts/Navigation/NavigationBar/NavigationIconLink.tsx": 1, "components/layouts/ProjectLayout/BuildingState.tsx": 1, "components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/mobileOrgMenuRegistry.tsx": 2, - "components/layouts/ReportsLayout/ReportsMenu.tsx": 1, "components/layouts/SQLEditorLayout/SQLEditorMenu.tsx": 2, "components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx": 1, "components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.utils.ts": 2, @@ -548,13 +540,12 @@ "data/profile/profile-identities-query.ts": 1, "data/profile/profile-unlink-identity-mutation.ts": 2, "data/projects/project-create-mutation.ts": 1, - "data/projects/project-detail-query.ts": 2, "data/replication/restart-pipeline-helper.ts": 2, "data/replication/rollback-tables-mutation.ts": 1, "data/reports/v2/auth.config.ts": 15, "data/reports/v2/edge-functions.config.ts": 9, "data/reports/v2/reports.types.ts": 2, - "data/sql/execute-sql-query.ts": 5, + "data/sql/execute-sql-mutation.ts": 5, "data/ssl-enforcement/ssl-enforcement-query.ts": 2, "data/ssl-enforcement/ssl-enforcement-update-mutation.ts": 1, "data/sso/sso-config-query.ts": 2, @@ -569,7 +560,6 @@ "data/table-rows/table-row-create-mutation.ts": 1, "data/table-rows/table-row-update-mutation.ts": 2, "data/table-rows/table-rows-query.ts": 5, - "data/tables/tables-query.ts": 1, "data/views/views-query.ts": 1, "hooks/analytics/useDbQuery.tsx": 1, "hooks/analytics/useLogsPreview.tsx": 1, @@ -611,7 +601,6 @@ "pages/api/platform/projects/[ref]/databases.ts": 1, "pages/integrations/vercel/[slug]/deploy-button/new-project.tsx": 1, "pages/integrations/vercel/[slug]/marketplace/choose-project.tsx": 1, - "pages/new/index.tsx": 1, "pages/project/[ref]/functions/[functionSlug]/index.tsx": 3, "pages/project/[ref]/settings/log-drains.tsx": 1, "state/ai-assistant-state.tsx": 4, @@ -664,7 +653,6 @@ "components/interfaces/Database/EnumeratedTypes/EditEnumeratedTypeSidePanel.tsx": 1, "components/interfaces/Database/EnumeratedTypes/EnumeratedTypeValueRow.tsx": 1, "components/interfaces/Database/Extensions/ExtensionCardSkeleton.tsx": 1, - "components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx": 1, "components/interfaces/Database/Migrations/Migrations.tsx": 1, "components/interfaces/Database/Privileges/PrivilegesHead.tsx": 1, "components/interfaces/Database/Privileges/PrivilegesTable.tsx": 1, @@ -683,7 +671,6 @@ "components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview.tsx": 1, "components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx": 1, "components/interfaces/Functions/FunctionsNav.tsx": 1, - "components/interfaces/Home/ProjectUsage.tsx": 1, "components/interfaces/Integrations/CronJobs/CronJobsEmptyState.tsx": 1, "components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx": 1, "components/interfaces/Integrations/Integration/LegacyIntegrationPage.tsx": 1, @@ -698,7 +685,6 @@ "components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx": 1, "components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx": 1, "components/interfaces/Organization/BillingSettings/CreditBalance.tsx": 1, - "components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx": 1, "components/interfaces/Organization/BillingSettings/Subscription/MembersExceedLimitModal.tsx": 1, "components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx": 1, "components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx": 1, @@ -761,6 +747,7 @@ "components/interfaces/Settings/Logs/LogColumnRenderers/DefaultPreviewColumnRenderer.tsx": 1, "components/interfaces/Settings/Logs/LogColumnRenderers/FunctionsEdgeColumnRender.tsx": 1, "components/interfaces/Settings/Logs/LogColumnRenderers/FunctionsLogsColumnRender.tsx": 1, + "components/interfaces/Settings/Logs/LogColumnRenderers/MultigresColumnRender.tsx": 1, "components/interfaces/Settings/Logs/LogSelection.tsx": 1, "components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx": 1, "components/interfaces/Settings/Logs/Logs.Divider.tsx": 1, @@ -815,10 +802,7 @@ "components/layouts/ProjectLayout/RestartingState.tsx": 1, "components/layouts/ProjectSettingsLayout/SettingsLayout.tsx": 1, "components/layouts/RealtimeLayout/RealtimeLayout.tsx": 1, - "components/layouts/ReportsLayout/ReportsMenu.tsx": 1, "components/layouts/SQLEditorLayout/SQLEditorLayout.tsx": 1, - "components/layouts/SignInLayout/ForgotPasswordLayout.tsx": 1, - "components/layouts/SignInLayout/SignInLayout.tsx": 1, "components/layouts/StorageLayout/StorageLayout.tsx": 1, "components/layouts/WizardLayout.tsx": 1, "components/to-be-cleaned/KeyMap.tsx": 1, @@ -873,7 +857,6 @@ "components/grid/components/header/filter/FilterPopoverNew.tsx": 1, "components/interfaces/Account/Preferences/ThemeSettings.tsx": 1, "components/interfaces/App/CommandMenu/ContextSearchResults.shared.tsx": 1, - "components/interfaces/Connect/ConnectTabContent.tsx": 1, "components/interfaces/ConnectSheet/ConnectStepsSection.tsx": 1, "components/interfaces/Database/Migrations/MigrationsEmptyState.tsx": 3, "components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx": 1, diff --git a/apps/studio/.gitignore b/apps/studio/.gitignore index 39577a49f19..55e8f54894a 100644 --- a/apps/studio/.gitignore +++ b/apps/studio/.gitignore @@ -15,6 +15,8 @@ next-env.d.ts # production /build +/dist +/.output # misc .DS_Store diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md new file mode 100644 index 00000000000..2d49fd90c13 --- /dev/null +++ b/apps/studio/TANSTACK_MIGRATION.md @@ -0,0 +1,514 @@ +# TanStack Start migration — route checklist + +Temporary tracking doc. Delete once migration is done. + +**Runtime model — Next.js and TanStack Start run side-by-side** + +Throughout this migration both runtimes coexist in the same workspace: + +- The Next.js pages router (`pages/...`) and the TanStack route tree + (`routes/...`) ship **at the same time**. The Vite/TanStack build is + what we run today; the Next build (`build:next` / `dev:next` scripts + in `apps/studio/package.json`) stays alive as a fallback so we can + bisect regressions and ship either runtime if needed. +- **Do not delete any `apps/studio/pages/...` file during the per-route + migration.** Path A pages re-export their `pages/` default export, so + the Next file is load-bearing for both runtimes. Removing it breaks + the Next build and breaks the TanStack route too. +- Body-moves and `pages/...` deletion happen **only in the final + cleanup pass**, after every route is represented in `routes/...` and + we're ready to retire the Next runtime entirely. That's a separate, + deliberate phase — not something to fold into individual route PRs. +- Same rule for the Next compat shims (`apps/studio/compat/next/`): + they stay until the cleanup pass, regardless of how many routes have + moved. + +**Strategy — minimum-diff re-export** + +The goal is to flip URL ownership to TanStack without rewriting page internals yet. For each page we pick one of two paths: + +- **Path A — re-export from `pages/`** (default). The TanStack route imports the page's default export from `apps/studio/pages/...` and renders it inside a thin wrapper component used as the route's `component`. `getLayout` is dropped on the floor — the TanStack layout chain (pathless `_app.tsx` / `_auth.tsx` + sibling-file layouts) handles wrapping instead. The page's Next-specific imports keep working via the `compat/next/` shim. Because `NextPageWithLayout` declares `{ dehydratedState: any }` as required props, pass `dehydratedState={undefined}` in the wrapper. +- **Path B — direct component import**. When the `pages/...` file is essentially `export default SomeComponent` re-exporting a component from elsewhere (typical for thin page wrappers), skip the middle-man and import `SomeComponent` directly in the TanStack route. + +We still need to land the shared layouts up-front: + +- Pathless layout routes (`_app.tsx`, `_auth.tsx`) hold shared shells without contributing URL segments. +- Sibling-file layouts: `segment.tsx` next to a `segment/` directory provides the layout with `` for children in that directory (e.g. `_app/account.tsx` wraps `_app/account/me.tsx`). No `route.tsx` files. +- Each product layout (DatabaseLayout, AuthLayout, SQLEditorLayout, …) becomes one sibling-file layout. + +Once every page is represented in `routes/`, we do a second pass to properly move the page body into the route file and delete `pages/...`. Path-tracking (A vs B) below tells us which pages still have live Next files we need to eliminate. + +Other rules: + +- New code uses native TanStack APIs directly (no `next/router`, no `next/link`). The Next compat shim stays in place for pages we re-export. +- `withAuth()` HOC → TanStack `beforeLoad` on the containing route/layout. Apply this at shared-layout level where possible. +- **Never** delete a `pages/...` file mid-migration — both runtimes need to keep working. Body-moves and Next-file deletions are reserved for the cleanup pass at the very end, after every entry in this checklist is `[x]`. See "Runtime model" above. +- Not migrated via this list: `pages/api/**` (Next API routes — separate migration), `_app.tsx`, `_document.tsx`, `_error`, `pages/org/_/[[...routeSlug]].tsx`, `pages/project/_/[[...routeSlug]].tsx` (catch-alls — revisit at the end). + +**Legend** + +- `[ ]` not started +- `[~]` in progress +- `[x] A` done — re-exported from `pages/...` (Next file still exists, needs body-move later) +- `[x] A→done` done & body moved — Next file deleted +- `[x] B` done — direct component import, no Next file involved (or Next file already deletable) + +--- + +## Shared layouts + +These are the layout-only TanStack files. Most hold a single product layout component. + +### App shell (pathless) + +- [x] `routes/_app.tsx` — AppLayout + DefaultLayout (reads `defaultLayoutHeaderTitle`/`hideMobileMenu` from leaf `staticData`) +- [x] `routes/_app/account.tsx` — AccountLayout (reads `accountLayoutTitle` from leaf `staticData`) +- [x] `routes/_app/org.tsx` — OrganizationLayout (reads `orgLayoutTitle` from leaf `staticData`). **Delta vs plan:** placed at `_app/org.tsx` (wraps both `/org/` index and `/org/$slug/*`) instead of `_app/org/$slug.tsx`. PageLayout stays inline on `/org/$slug/index.tsx` since only that one route uses it. +- [x] `routes/_app/new.tsx` — skipped; only `_app/new/index.tsx` lives under \_app (inlines WizardLayout). `new/$slug` is top-level (no AppLayout) so a sub-shell would not actually share state. +- [x] `routes/integrations/vercel.tsx` — VercelIntegrationWindowLayout. **Delta vs plan:** placed at top-level rather than under `_app/` — Next getLayout for all three leaves wraps only in VercelIntegrationWindowLayout, no AppLayout/DefaultLayout. + +### Project shell + +- [x] `routes/project/$ref.tsx` — DefaultLayout only. **Delta vs plan:** ProjectLayoutWithAuth omitted from the shell because product layouts (DatabaseLayout, AuthLayout, StorageLayout, …) already render `withAuth(... ProjectLayout ...)` internally — adding it here would double-wrap. The home page (`/project/$ref/index.tsx`) wraps itself in `ProjectLayoutWithAuth` since it has no product layout. +- [x] `routes/project/$ref/database.tsx` — DatabaseLayout (reads `databaseLayoutTitle` from leaf `staticData`) +- [x] `routes/project/$ref/database/triggers.tsx` — sub-shell with `PageLayout` + permission gate + nav items, inlined from `DatabaseTriggersLayout`. **Delta vs plan:** the existing `DatabaseTriggersLayout` component wraps `` internally, so re-using it inside the database.tsx shell would double-wrap. Inlined the inner part instead; the Next-side component is left untouched (still used by the `pages/...` files we re-export). +- [x] `routes/project/$ref/auth.tsx` — AuthLayout (reads `authLayoutTitle` from leaf `staticData`). **Delta vs plan:** shell honours a `skipAuthLayout: true` opt-out in `staticData` for leaves whose own body or sub-layout already wraps in `AuthLayout` (`AuthProvidersLayout`, `AuthEmailsLayout`, `pages/.../auth/third-party.tsx`) — without it those routes would double-wrap (which also doubles `withAuth` + `ProjectLayout`). +- ~~`routes/project/$ref/auth/templates.tsx` — AuthEmailsLayout~~ **Delta vs plan: not landed.** A unified `templates.tsx` sub-shell would force `templates/$templateId.tsx` (which uses plain `AuthLayout`, not `AuthEmailsLayout`) into the wrong wrapping. Instead `templates/index.tsx` and `auth/smtp.tsx` each set `skipAuthLayout: true` and wrap themselves in `AuthEmailsLayout`; `templates/$templateId.tsx` uses the standard auth shell with `authLayoutTitle: 'Emails'`. +- [x] `routes/project/$ref/storage.tsx` — StorageLayout + StorageBucketsLayout (reads `storageLayoutTitle`, optional `skipStorageBucketsLayout`, `storageBucketsLayoutTitle`, `storageBucketsLayoutHideSubtitle` from leaf `staticData`). **Delta vs plan:** the shell wraps in BOTH StorageLayout and StorageBucketsLayout by default — every storage page except bucket-detail pages uses both. Bucket-detail pages set `skipStorageBucketsLayout: true`. `/storage/s3` uses `storageBucketsLayout{Title,HideSubtitle}` to override the inner header. +- [x] `routes/project/$ref/realtime.tsx` — RealtimeLayout (reads `realtimeLayoutTitle` from leaf `staticData`) +- [x] `routes/project/$ref/functions.tsx` — EdgeFunctionsLayout (reads `functionsLayoutTitle` from leaf `staticData`). Honours `skipFunctionsLayout: true` opt-out for the `$functionSlug` subtree, whose `EdgeFunctionDetailsLayout` already wraps `EdgeFunctionsLayout` internally — same pattern as auth.tsx. Sub-shell at `routes/project/$ref/functions/$functionSlug.tsx` provides `EdgeFunctionDetailsLayout` for all 5 slug leaves (reads `edgeFunctionDetailsTitle` from leaf staticData). +- [x] `routes/project/$ref/branches.tsx` — BranchLayout only. **Delta vs plan:** the per-page `PageLayout` (with different titles + primary/secondary actions) stays in each leaf. Hoisted `BranchesPageWrapper` and `MergeRequestsPageWrapper` to top-level exports in their respective `pages/...` files so the route files can import + re-use the same wrapping. +- [x] `routes/project/$ref/logs.tsx` — LogsLayout (reads `logsLayoutTitle` from leaf staticData). Honours `skipLogsLayout: true` for `logs/index` (page handles its own ProjectLayout-wrapped content for the UnifiedLogs / no-permission cases). Refactored `pages/.../logs/index.tsx` to move the inline `` into `getLayout` so it isn't duplicated when the TanStack project shell already provides DefaultLayout. +- [x] `routes/project/$ref/observability.tsx` — ObservabilityLayout (reads `observabilityLayoutTitle` from leaf staticData) +- [x] `routes/project/$ref/advisors.tsx` — AdvisorsLayout (reads `advisorsLayoutTitle` from leaf staticData). Honours `skipAdvisorsLayout: true` opt-out for the rules sub-shell, which provides its own AdvisorsLayout-less-DefaultLayout wrap. Scans whole match chain (same pattern as functions.tsx). +- [x] `routes/project/$ref/advisors/rules.tsx` — sub-shell that inlines the inner body of `AdvisorRulesLayout` (AdvisorsLayout + PageLayout with title/tabs/feature-preview badge), minus the outer DefaultLayout (already provided by the parent project shell). Sets `skipAdvisorsLayout: true` on its own staticData. **Delta vs plan:** the existing `AdvisorRulesLayout` component wraps in DefaultLayout + AdvisorsLayout internally, so reusing it as-is would double-wrap both. Inlined the inner part; the Next-side component is untouched. +- [x] `routes/project/$ref/settings.tsx` — SettingsLayout (reads `settingsLayoutTitle` from leaf staticData). Honours `skipSettingsLayout: true` for `settings/api` (redirect-only page). Adds a sub-shell at `routes/project/$ref/settings/api-keys.tsx` providing `ApiKeysLayout` for both api-keys leaves; `jwt/index` wraps in `JWTKeysLayout` inline since `jwt/legacy` doesn't share it. +- [x] `routes/project/$ref/integrations.tsx` — ProjectIntegrationsLayout (no staticData; all 4 leaves share identical layout). Layout is `withAuth(({ children }) => {children})`, so the shell just wraps `` once. +- [x] `routes/project/$ref/sql.tsx` — EditorBaseLayout + SQLEditorLayout. Twin of editor.tsx; all four leaves share identical layout props so the shell hardcodes them (no `staticData` overrides). EditorBaseLayout wraps in ProjectLayoutWithAuth; SQLEditorLayout adds its own `withAuth` HOC but no extra ProjectLayout — same shape as the table editor (auth check runs twice but no double render). +- [x] `routes/project/$ref/editor.tsx` — EditorBaseLayout + TableEditorLayout. All three leaves share identical layout props so the shell hardcodes them (no `staticData` overrides). EditorBaseLayout wraps in `ProjectLayoutWithAuth` internally; TableEditorLayout's happy path is just a fragment + side-effect (banner) and only wraps in `ProjectLayoutWithAuth` on its no-permission branch — same as Next, no double-wrap in normal use. + +### Auth shell (pathless) + +- [x] `routes/_auth.tsx` — AuthenticationLayout + +--- + +## Pages + +### App shell — `/account/*` + +- [x] A `routes/_app/account/me.tsx` ← `pages/account/me.tsx` +- [x] A `routes/_app/account/security.tsx` ← `pages/account/security.tsx` +- [x] A `routes/_app/account/audit.tsx` ← `pages/account/audit.tsx` +- [x] A `routes/_app/account/tokens/index.tsx` ← `pages/account/tokens.tsx` +- [x] A `routes/_app/account/tokens/scoped.tsx` ← `pages/account/tokens/scoped.tsx` + +### App shell — `/org/$slug/*` + +- [x] A `routes/_app/org/$slug/index.tsx` ← `pages/org/[slug]/index.tsx` +- [x] A `routes/_app/org/$slug/apps.tsx` ← `pages/org/[slug]/apps.tsx` +- [x] A `routes/_app/org/$slug/audit.tsx` ← `pages/org/[slug]/audit.tsx` +- [x] A `routes/_app/org/$slug/billing.tsx` ← `pages/org/[slug]/billing.tsx` +- [x] A `routes/_app/org/$slug/documents.tsx` ← `pages/org/[slug]/documents.tsx` +- [x] A `routes/_app/org/$slug/general.tsx` ← `pages/org/[slug]/general.tsx` +- [x] A `routes/_app/org/$slug/integrations.tsx` ← `pages/org/[slug]/integrations.tsx` +- [x] A `routes/_app/org/$slug/security.tsx` ← `pages/org/[slug]/security.tsx` +- [x] A `routes/_app/org/$slug/sso.tsx` ← `pages/org/[slug]/sso.tsx` +- [x] A `routes/_app/org/$slug/team.tsx` ← `pages/org/[slug]/team.tsx` +- [x] A `routes/_app/org/$slug/usage.tsx` ← `pages/org/[slug]/usage.tsx` +- [x] A `routes/_app/org/$slug/private-apps/index.tsx` ← `pages/org/[slug]/private-apps/index.tsx` +- [x] A `routes/_app/org/$slug/webhooks/index.tsx` ← `pages/org/[slug]/webhooks/index.tsx` +- [x] A `routes/_app/org/$slug/webhooks/$endpointId.tsx` ← `pages/org/[slug]/webhooks/[endpointId].tsx` +- [x] A `routes/_app/org/index.tsx` ← `pages/org/index.tsx` (redirect) + +### App shell — top-level pages + +- [x] A `routes/_app/organizations.tsx` ← `pages/organizations.tsx` (page default already withAuth-wrapped; PageLayout wraps body) +- [x] `routes/_app/new/index.tsx` ← `pages/new/index.tsx` (inlines WizardLayout; sets `defaultLayoutHeaderTitle: 'New organization'` + `hideMobileMenu: true` on staticData). **Delta vs plan:** no `_app/new.tsx` sub-shell — `new/$slug` doesn't fit under \_app and uses a different inner wrapper (PageLayout), so a shared shell wouldn't share anything. +- [x] A `routes/new/$slug.tsx` ← `pages/new/[slug].tsx` **Delta vs plan:** placed at top-level rather than under `_app/` — Next getLayout omits AppLayout and uses PageLayout (not WizardLayout) inside DefaultLayout, so leaf inlines the full DefaultLayout + PageLayout wrap itself. +- [x] A `routes/aws-marketplace-onboarding.tsx` ← `pages/aws-marketplace-onboarding.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses its own `LinkAwsMarketplaceLayout` and doesn't want `AppLayout` + `DefaultLayout` wrapping. +- [x] A `routes/claim-project.tsx` ← `pages/claim-project.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses its own `` + `
` layout and doesn't want `AppLayout` + `DefaultLayout` wrapping. +- [x] A `routes/join.tsx` ← `pages/join.tsx` **Delta vs plan:** placed at root rather than under `_app/` — page uses a centered-div layout and doesn't want `AppLayout` + `DefaultLayout` wrapping. +- [x] `routes/_app/support/new.tsx` ← `pages/support/new.tsx` (sets `hideMobileMenu: true` staticData; existing page is `withAuth`-wrapped so no beforeLoad migration needed yet) +- [x] `routes/_app/support/link.tsx` ← `pages/support/link.tsx` + +### App shell — integrations + +- [x] A `routes/integrations/vercel/install.tsx` ← `pages/integrations/vercel/install.tsx` +- [x] A `routes/integrations/vercel/$slug/marketplace/choose-project.tsx` ← `pages/integrations/vercel/[slug]/marketplace/choose-project.tsx` +- [x] A `routes/integrations/vercel/$slug/deploy-button/new-project.tsx` ← `pages/integrations/vercel/[slug]/deploy-button/new-project.tsx` +- [x] A `routes/integrations/github/authorize.tsx` ← `pages/integrations/github/authorize.tsx` **Delta vs plan:** placed at top-level rather than under `_app/` — Next page has no getLayout (renders bare), so adding AppLayout/DefaultLayout via \_app would be a behaviour change. + +### Project shell — home + +- [x] A `routes/project/$ref/index.tsx` ← `pages/project/[ref]/index.tsx` (route wraps in `ProjectLayoutWithAuth` itself — see shell delta above) +- [x] `routes/project/$ref/merge.tsx` ← `pages/project/[ref]/merge.tsx` (leaf wraps body in `ProjectLayoutWithAuth`; parent `project/$ref.tsx` shell provides DefaultLayout) + +### Project shell — `/api/*` + +- [x] `routes/project/$ref/api/index.tsx` ← `pages/project/[ref]/api/index.tsx` (redirect-only page; no extra wrap needed beyond the parent DefaultLayout shell) + +### Project shell — `/database/*` + +- [x] A `routes/project/$ref/database/schemas.tsx` ← `pages/project/[ref]/database/schemas.tsx` +- [x] A `routes/project/$ref/database/extensions.tsx` ← `pages/project/[ref]/database/extensions.tsx` +- [x] A `routes/project/$ref/database/functions.tsx` ← `pages/project/[ref]/database/functions.tsx` +- [x] A `routes/project/$ref/database/indexes.tsx` ← `pages/project/[ref]/database/indexes.tsx` +- [x] A `routes/project/$ref/database/migrations.tsx` ← `pages/project/[ref]/database/migrations.tsx` +- [x] A `routes/project/$ref/database/roles.tsx` ← `pages/project/[ref]/database/roles.tsx` +- [x] A `routes/project/$ref/database/settings.tsx` ← `pages/project/[ref]/database/settings.tsx` +- [x] A `routes/project/$ref/database/types.tsx` ← `pages/project/[ref]/database/types.tsx` +- [x] A `routes/project/$ref/database/column-privileges.tsx` ← `pages/project/[ref]/database/column-privileges.tsx` +- [x] A `routes/project/$ref/database/tables/index.tsx` ← `pages/project/[ref]/database/tables/index.tsx` +- [x] A `routes/project/$ref/database/tables/$id.tsx` ← `pages/project/[ref]/database/tables/[id].tsx` +- [x] A `routes/project/$ref/database/publications/index.tsx` ← `pages/project/[ref]/database/publications/index.tsx` +- [x] A `routes/project/$ref/database/publications/$id.tsx` ← `pages/project/[ref]/database/publications/[id].tsx` +- [x] A `routes/project/$ref/database/replication/index.tsx` ← `pages/project/[ref]/database/replication/index.tsx` +- [x] A `routes/project/$ref/database/replication/$pipelineId.tsx` ← `pages/project/[ref]/database/replication/[pipelineId].tsx` +- [x] A `routes/project/$ref/database/replication/replica/$replicaId.tsx` ← `pages/project/[ref]/database/replication/replica/[replicaId].tsx` +- [x] A `routes/project/$ref/database/triggers/index.tsx` ← `pages/project/[ref]/database/triggers/index.tsx` +- [x] A `routes/project/$ref/database/triggers/data.tsx` ← `pages/project/[ref]/database/triggers/data.tsx` (sub-shell at `database/triggers.tsx` provides PageLayout + nav, parent shell provides DatabaseLayout) +- [x] A `routes/project/$ref/database/triggers/event.tsx` ← `pages/project/[ref]/database/triggers/event.tsx` (same as data) +- [x] A `routes/project/$ref/database/backups/pitr.tsx` ← `pages/project/[ref]/database/backups/pitr.tsx` +- [x] A `routes/project/$ref/database/backups/restore-to-new-project.tsx` ← `pages/project/[ref]/database/backups/restore-to-new-project.tsx` +- [x] A `routes/project/$ref/database/backups/scheduled.tsx` ← `pages/project/[ref]/database/backups/scheduled.tsx` + +### Project shell — `/auth/*` + +- [x] A `routes/project/$ref/auth/overview.tsx` ← `pages/project/[ref]/auth/overview.tsx` +- [x] A `routes/project/$ref/auth/users.tsx` ← `pages/project/[ref]/auth/users.tsx` +- [x] A `routes/project/$ref/auth/policies.tsx` ← `pages/project/[ref]/auth/policies.tsx` +- [x] A `routes/project/$ref/auth/providers.tsx` ← `pages/project/[ref]/auth/providers.tsx` (sets `skipAuthLayout: true`, wraps in `AuthProvidersLayout` directly) +- [x] A `routes/project/$ref/auth/mfa.tsx` ← `pages/project/[ref]/auth/mfa.tsx` +- [x] A `routes/project/$ref/auth/hooks.tsx` ← `pages/project/[ref]/auth/hooks.tsx` +- [x] A `routes/project/$ref/auth/smtp.tsx` ← `pages/project/[ref]/auth/smtp.tsx` (sets `skipAuthLayout: true`, wraps in `AuthEmailsLayout` directly) +- [x] A `routes/project/$ref/auth/sessions.tsx` ← `pages/project/[ref]/auth/sessions.tsx` +- [x] A `routes/project/$ref/auth/passkeys.tsx` ← `pages/project/[ref]/auth/passkeys.tsx` +- [x] A `routes/project/$ref/auth/performance.tsx` ← `pages/project/[ref]/auth/performance.tsx` +- [x] A `routes/project/$ref/auth/protection.tsx` ← `pages/project/[ref]/auth/protection.tsx` +- [x] A `routes/project/$ref/auth/rate-limits.tsx` ← `pages/project/[ref]/auth/rate-limits.tsx` +- [x] A `routes/project/$ref/auth/third-party.tsx` ← `pages/project/[ref]/auth/third-party.tsx` (sets `skipAuthLayout: true` — page body inlines `` which already wraps ``) +- [x] A `routes/project/$ref/auth/oauth-apps.tsx` ← `pages/project/[ref]/auth/oauth-apps.tsx` +- [x] A `routes/project/$ref/auth/oauth-server.tsx` ← `pages/project/[ref]/auth/oauth-server.tsx` +- [x] A `routes/project/$ref/auth/url-configuration.tsx` ← `pages/project/[ref]/auth/url-configuration.tsx` +- [x] A `routes/project/$ref/auth/audit-logs.tsx` ← `pages/project/[ref]/auth/audit-logs.tsx` +- [x] A `routes/project/$ref/auth/templates/index.tsx` ← `pages/project/[ref]/auth/templates/index.tsx` (sets `skipAuthLayout: true`, wraps in `AuthEmailsLayout` directly) +- [x] A `routes/project/$ref/auth/templates/$templateId.tsx` ← `pages/project/[ref]/auth/templates/[templateId].tsx` (`authLayoutTitle: 'Emails'` — page uses plain `AuthLayout`, not `AuthEmailsLayout`) + +### Project shell — `/storage/*` + +- [x] A `routes/project/$ref/storage/s3.tsx` ← `pages/project/[ref]/storage/s3.tsx` +- [x] A `routes/project/$ref/storage/files/index.tsx` ← `pages/project/[ref]/storage/files/index.tsx` +- [x] A `routes/project/$ref/storage/files/policies.tsx` ← `pages/project/[ref]/storage/files/policies.tsx` +- [x] A `routes/project/$ref/storage/files/settings.tsx` ← `pages/project/[ref]/storage/files/settings.tsx` +- [x] A `routes/project/$ref/storage/files/buckets/$bucketId.tsx` ← `pages/project/[ref]/storage/files/buckets/[bucketId].tsx` (sets `skipStorageBucketsLayout: true`) +- [x] A `routes/project/$ref/storage/analytics/index.tsx` ← `pages/project/[ref]/storage/analytics/index.tsx` +- [x] A `routes/project/$ref/storage/analytics/buckets/$bucketId.tsx` ← `pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx` (sets `skipStorageBucketsLayout: true`) +- [x] A `routes/project/$ref/storage/vectors/index.tsx` ← `pages/project/[ref]/storage/vectors/index.tsx` +- [x] A `routes/project/$ref/storage/vectors/buckets/$bucketId.tsx` ← `pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx` (sets `skipStorageBucketsLayout: true`) + +### Project shell — `/realtime/*` + +- [x] A `routes/project/$ref/realtime/inspector.tsx` ← `pages/project/[ref]/realtime/inspector.tsx` +- [x] A `routes/project/$ref/realtime/policies.tsx` ← `pages/project/[ref]/realtime/policies.tsx` +- [x] A `routes/project/$ref/realtime/settings.tsx` ← `pages/project/[ref]/realtime/settings.tsx` + +### Project shell — `/functions/*` + +- [x] A `routes/project/$ref/functions/index.tsx` ← `pages/project/[ref]/functions/index.tsx` (route wraps in exported `EdgeFunctionsIndexPageWrapper` for the inline PageHeader + actions) +- [x] A `routes/project/$ref/functions/new.tsx` ← `pages/project/[ref]/functions/new.tsx` +- [x] A `routes/project/$ref/functions/secrets.tsx` ← `pages/project/[ref]/functions/secrets.tsx` (route wraps in exported `SecretsPageWrapper`) +- [x] A `routes/project/$ref/functions/$functionSlug/index.tsx` ← `pages/project/[ref]/functions/[functionSlug]/index.tsx` +- [x] A `routes/project/$ref/functions/$functionSlug/code.tsx` ← `pages/project/[ref]/functions/[functionSlug]/code.tsx` +- [x] A `routes/project/$ref/functions/$functionSlug/details.tsx` ← `pages/project/[ref]/functions/[functionSlug]/details.tsx` +- [x] A `routes/project/$ref/functions/$functionSlug/invocations.tsx` ← `pages/project/[ref]/functions/[functionSlug]/invocations.tsx` +- [x] A `routes/project/$ref/functions/$functionSlug/logs.tsx` ← `pages/project/[ref]/functions/[functionSlug]/logs.tsx` + +### Project shell — `/branches/*` + +- [x] A `routes/project/$ref/branches/index.tsx` ← `pages/project/[ref]/branches/index.tsx` (route wraps in exported `BranchesPageWrapper` to preserve the page's `PageLayout` + Create-branch action) +- [x] A `routes/project/$ref/branches/merge-requests.tsx` ← `pages/project/[ref]/branches/merge-requests.tsx` (route wraps in exported `MergeRequestsPageWrapper`) + +### Project shell — `/logs/*` + +- [x] A `routes/project/$ref/logs/index.tsx` ← `pages/project/[ref]/logs/index.tsx` (sets `skipLogsLayout: true`; page handles its own ProjectLayout; DefaultLayout moved to page's `getLayout` so Next still wraps it) +- [x] A `routes/project/$ref/logs/auth-logs.tsx` ← `pages/project/[ref]/logs/auth-logs.tsx` +- [x] A `routes/project/$ref/logs/cron-logs.tsx` ← `pages/project/[ref]/logs/cron-logs.tsx` +- [x] A `routes/project/$ref/logs/dedicated-pooler-logs.tsx` ← `pages/project/[ref]/logs/dedicated-pooler-logs.tsx` +- [x] A `routes/project/$ref/logs/edge-functions-logs.tsx` ← `pages/project/[ref]/logs/edge-functions-logs.tsx` +- [x] A `routes/project/$ref/logs/edge-logs.tsx` ← `pages/project/[ref]/logs/edge-logs.tsx` +- [x] A `routes/project/$ref/logs/pg-upgrade-logs.tsx` ← `pages/project/[ref]/logs/pg-upgrade-logs.tsx` +- [x] A `routes/project/$ref/logs/pgcron-logs.tsx` ← `pages/project/[ref]/logs/pgcron-logs.tsx` +- [x] A `routes/project/$ref/logs/pooler-logs.tsx` ← `pages/project/[ref]/logs/pooler-logs.tsx` +- [x] A `routes/project/$ref/logs/postgres-logs.tsx` ← `pages/project/[ref]/logs/postgres-logs.tsx` +- [x] A `routes/project/$ref/logs/postgrest-logs.tsx` ← `pages/project/[ref]/logs/postgrest-logs.tsx` +- [x] A `routes/project/$ref/logs/realtime-logs.tsx` ← `pages/project/[ref]/logs/realtime-logs.tsx` +- [x] A `routes/project/$ref/logs/replication-logs.tsx` ← `pages/project/[ref]/logs/replication-logs.tsx` +- [x] A `routes/project/$ref/logs/storage-logs.tsx` ← `pages/project/[ref]/logs/storage-logs.tsx` +- [x] A `routes/project/$ref/logs/explorer/index.tsx` ← `pages/project/[ref]/logs/explorer/index.tsx` +- [x] A `routes/project/$ref/logs/explorer/recent.tsx` ← `pages/project/[ref]/logs/explorer/recent.tsx` +- [x] A `routes/project/$ref/logs/explorer/saved.tsx` ← `pages/project/[ref]/logs/explorer/saved.tsx` +- [x] A `routes/project/$ref/logs/explorer/templates.tsx` ← `pages/project/[ref]/logs/explorer/templates.tsx` + +### Project shell — `/observability/*` + +- [x] A `routes/project/$ref/observability/index.tsx` ← `pages/project/[ref]/observability/index.tsx` +- [x] A `routes/project/$ref/observability/$id.tsx` ← `pages/project/[ref]/observability/[id].tsx` +- [x] A `routes/project/$ref/observability/auth.tsx` ← `pages/project/[ref]/observability/auth.tsx` +- [x] A `routes/project/$ref/observability/database.tsx` ← `pages/project/[ref]/observability/database.tsx` +- [x] A `routes/project/$ref/observability/api-overview.tsx` ← `pages/project/[ref]/observability/api-overview.tsx` +- [x] A `routes/project/$ref/observability/edge-functions.tsx` ← `pages/project/[ref]/observability/edge-functions.tsx` +- [x] A `routes/project/$ref/observability/postgrest.tsx` ← `pages/project/[ref]/observability/postgrest.tsx` +- [x] A `routes/project/$ref/observability/query-insights.tsx` ← `pages/project/[ref]/observability/query-insights.tsx` +- [x] A `routes/project/$ref/observability/query-performance.tsx` ← `pages/project/[ref]/observability/query-performance.tsx` +- [x] A `routes/project/$ref/observability/realtime.tsx` ← `pages/project/[ref]/observability/realtime.tsx` +- [x] A `routes/project/$ref/observability/storage.tsx` ← `pages/project/[ref]/observability/storage.tsx` + +### Project shell — `/advisors/*` + +- [x] A `routes/project/$ref/advisors/performance.tsx` ← `pages/project/[ref]/advisors/performance.tsx` +- [x] A `routes/project/$ref/advisors/security.tsx` ← `pages/project/[ref]/advisors/security.tsx` +- [x] A `routes/project/$ref/advisors/rules/performance.tsx` ← `pages/project/[ref]/advisors/rules/performance.tsx` +- [x] A `routes/project/$ref/advisors/rules/security.tsx` ← `pages/project/[ref]/advisors/rules/security.tsx` + +### Project shell — `/settings/*` + +- [x] A `routes/project/$ref/settings/general.tsx` ← `pages/project/[ref]/settings/general.tsx` +- [x] A `routes/project/$ref/settings/addons.tsx` ← `pages/project/[ref]/settings/addons.tsx` +- [x] A `routes/project/$ref/settings/api.tsx` ← `pages/project/[ref]/settings/api.tsx` (sets `skipSettingsLayout: true` — page is a useEffect redirect) +- [x] A `routes/project/$ref/settings/compute-and-disk.tsx` ← `pages/project/[ref]/settings/compute-and-disk.tsx` +- [x] A `routes/project/$ref/settings/dashboard.tsx` ← `pages/project/[ref]/settings/dashboard.tsx` +- [x] A `routes/project/$ref/settings/infrastructure.tsx` ← `pages/project/[ref]/settings/infrastructure.tsx` +- [x] A `routes/project/$ref/settings/integrations.tsx` ← `pages/project/[ref]/settings/integrations.tsx` +- [x] A `routes/project/$ref/settings/log-drains.tsx` ← `pages/project/[ref]/settings/log-drains.tsx` +- [x] A `routes/project/$ref/settings/api-keys/index.tsx` ← `pages/project/[ref]/settings/api-keys/index.tsx` (under `api-keys.tsx` sub-shell with ApiKeysLayout) +- [x] A `routes/project/$ref/settings/api-keys/legacy.tsx` ← `pages/project/[ref]/settings/api-keys/legacy.tsx` (under `api-keys.tsx` sub-shell) +- [x] A `routes/project/$ref/settings/billing/usage.tsx` ← `pages/project/[ref]/settings/billing/usage.tsx` +- [x] A `routes/project/$ref/settings/jwt/index.tsx` ← `pages/project/[ref]/settings/jwt/index.tsx` (wraps in JWTKeysLayout inline) +- [x] A `routes/project/$ref/settings/jwt/legacy.tsx` ← `pages/project/[ref]/settings/jwt/legacy.tsx` +- [x] A `routes/project/$ref/settings/webhooks/index.tsx` ← `pages/project/[ref]/settings/webhooks/index.tsx` +- [x] A `routes/project/$ref/settings/webhooks/$endpointId.tsx` ← `pages/project/[ref]/settings/webhooks/[endpointId].tsx` + +### Project shell — `/integrations/*` + +- [x] `routes/project/$ref/integrations/index.tsx` ← `pages/project/[ref]/integrations/index.tsx` +- [x] `routes/project/$ref/integrations/$id/index.tsx` ← `pages/project/[ref]/integrations/[id]/index.tsx` +- [x] `routes/project/$ref/integrations/$id/$pageId/index.tsx` ← `pages/project/[ref]/integrations/[id]/[pageId]/index.tsx` +- [x] `routes/project/$ref/integrations/$id/$pageId/$childId/index.tsx` ← `pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx` + +### Project shell — `/sql/*` + +- [x] A `routes/project/$ref/sql/index.tsx` ← `pages/project/[ref]/sql/index.tsx` +- [x] A `routes/project/$ref/sql/$id.tsx` ← `pages/project/[ref]/sql/[id].tsx` +- [x] A `routes/project/$ref/sql/templates.tsx` ← `pages/project/[ref]/sql/templates.tsx` +- [x] A `routes/project/$ref/sql/quickstarts.tsx` ← `pages/project/[ref]/sql/quickstarts.tsx` + +### Project shell — `/editor/*` + +- [x] A `routes/project/$ref/editor/index.tsx` ← `pages/project/[ref]/editor/index.tsx` +- [x] A `routes/project/$ref/editor/$id.tsx` ← `pages/project/[ref]/editor/[id].tsx` +- [x] A `routes/project/$ref/editor/new.tsx` ← `pages/project/[ref]/editor/new.tsx` + +### Auth shell — `/sign-in`, `/sign-up`, etc. + +- [x] A `routes/_auth/sign-in.tsx` ← `pages/sign-in.tsx` +- [x] A `routes/_auth/sign-up.tsx` ← `pages/sign-up.tsx` +- [x] A `routes/_auth/sign-in-sso.tsx` ← `pages/sign-in-sso.tsx` +- [x] A `routes/_auth/sign-in-partner.tsx` ← `pages/sign-in-partner.tsx` +- [x] A `routes/_auth/sign-in-mfa.tsx` ← `pages/sign-in-mfa.tsx` (page inlines SignInLayout) +- [x] A `routes/_auth/forgot-password.tsx` ← `pages/forgot-password.tsx` +- [x] A `routes/_auth/forgot-password-mfa.tsx` ← `pages/forgot-password-mfa.tsx` (page inlines ForgotPasswordLayout) +- [x] A `routes/_auth/reset-password.tsx` ← `pages/reset-password.tsx` (page default already withAuth-wrapped) +- [x] A `routes/_auth/cli/login.tsx` ← `pages/cli/login.tsx` (page inlines APIAuthorizationLayout, withAuth) +- [x] A `routes/_auth/partners/stripe/projects/login.tsx` ← `pages/partners/stripe/projects/login.tsx` (page inlines APIAuthorizationLayout, withAuth) + +### Standalone (no shared shell) + +- [x] B `routes/index.tsx` — redirect-only root route. Mirrors the Next.js `redirects()` rules in `next.config.ts`: platform sends users to `/org` (or `/new/new-project` when deep-linked with `?next=new-project`), self-hosted sends them to `/project/default`. **Follow-up:** the redirect targets currently use `href` (full reload) because they were on the Next side when this was written; switch to `to` now that all of them live in the TanStack tree. +- [x] A `routes/authorize.tsx` ← `pages/authorize.tsx` (APIAuthorizationLayout) +- [x] A `routes/redeem.tsx` ← `pages/redeem.tsx` (RedeemCreditsLayout) +- [x] A `routes/logout.tsx` ← `pages/logout.tsx` +- [x] A `routes/maintenance.tsx` ← `pages/maintenance.tsx` + +### Error pages (handled at root) + +- [x] A `__root.tsx` — wired `notFoundComponent` to `pages/404.tsx` +- [x] `__root.tsx` — wired `errorComponent` to `pages/500.tsx`. Mirrors the in-tree `react-error-boundary` Sentry capture (`scope.setTag('routerErrorComponent', true)`) so router-level errors (loader/component-render failures before the in-tree boundary mounts) still report. `pages/_error.jsx` stays load-bearing under Next but isn't reached at runtime under TanStack — it's the pages-router catch-all that has no TanStack equivalent. + +--- + +## API routes + +**Strategy — shim + re-export.** `compat/next/api.ts` exposes +`toWebHandler(nextHandler)` that adapts a `(req, res) => …` Next.js handler +into a TanStack Start Web-fetch handler. Each `routes/api/...` file imports +the default export from `pages/api/...`, wraps with `toWebHandler`, and +registers via `createFileRoute(...).server.handlers`. `apiWrapper` and +`apiAuthenticate` stay untouched — they run inside the shim, seeing a +NextApiRequest-shaped `req` and a proxy `res`. + +**Path conventions** — `pages/api/foo/[bar]/baz.ts` → `routes/api/foo/$bar/baz.ts`; +`pages/api/foo/[[...slug]].ts` → `routes/api/foo/$.ts`. + +**Shim coverage.** The proxy `req` / `res` cover both the buffered and +streaming patterns that pages-router handlers use: + +- **Buffered responses** — `res.status`/`setHeader`/`json`/`send`/`write`/ + `end` accumulate into a single `Response` body when the handler returns. +- **Streaming responses** — `res.writeHead(status, headers?)` (or + `res.flushHeaders()`) flips the proxy into streaming mode: a Web + `ReadableStream` opens, buffered chunks flush into it, subsequent + `res.write(chunk)` enqueues live, `res.end()` closes it. `finalize()` + returns the `Response` while the handler keeps pushing chunks. This is + what makes `result.pipeUIMessageStreamToResponse(res, …)` (AI SDK) + stream token-by-token to the browser. +- **Client abort** — Web `Request.signal` is plumbed through as + `req.on('close' | 'aborted', …)`. AI handlers that wire + `abortController.abort()` off those events keep working. +- **EventEmitter surface** — `req.on`/`once`/`off`/`emit` (events `close` + / `aborted` are real; other names accepted but no-op). `res.on`/etc. + are no-op stubs so pipe helpers attaching `drain`/`close`/`error` + listeners don't crash. +- **Body parsing** — JSON and `application/x-www-form-urlencoded` parsed + to `req.body`; everything else is the raw text. Multipart inbound is + not implemented — no studio handler reads multipart in. + +Two routes still bypass the shim because they're easier to write +Web-natively from scratch: + +- `routes/api/v1/projects/$ref/functions/$slug/body.ts` — multipart + streaming OUT (artifact download). Builds the `Response` body as a + `ReadableStream`; each artifact file converts via + `Readable.toWeb(createReadStream(...))` and pulls chunk-by-chunk into + the stream. +- `routes/api/mcp/index.ts` — uses MCP SDK's + `WebStandardStreamableHTTPServerTransport` (`handleRequest(request)` + returns a `Response` directly). +- `pages/api/ai/docs.ts` was already edge-runtime / Web-Response native + — direct re-export, no shim involved. + +Tracking below is coarse — each bullet is a `pages/api/**` subtree. Check off +once every file in the subtree has a `routes/api/**` counterpart. Expand into +per-file items only when a subtree has special cases. + +- [x] `routes/api/get-ip-address.ts` — canary port (validates the shim) +- [x] `routes/api/**` — root-level simple endpoints (`check-cname`, + `cli-release-version`, `enabled-features-overrides`, `generate-attachment-url`, + `get-deployment-commit`, `get-utc-time`, `status-override`) +- [x] `routes/api/ai/**` — AI endpoints (`docs.ts` direct-ported as Web-native) +- [x] `routes/api/connect/**` +- [x] `routes/api/content/**` +- [x] `routes/api/edge-functions/**` +- [x] `routes/api/integrations/**` +- [x] `routes/api/platform/**` (60 files, scripted port) +- [x] `routes/api/v1/**` — except `body.ts` (streaming rewrite) +- [x] `routes/api/v1/projects/$ref/functions/$slug/body.ts` — Web-streams rewrite. Returns a `Response` whose body is a `ReadableStream`; each artifact file is converted via `Readable.toWeb(createReadStream(...))` and pulled chunk-by-chunk into the multipart stream. Skips the `apiWrapper` since `getFunctionsArtifactStore` already asserts self-hosted mode (the pages-router `withAuth` was a no-op outside `IS_PLATFORM`). +- [x] `routes/api/mcp/index.ts` — uses `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js`. Takes a Web `Request`, returns a `Response` directly — no shim needed. Query parsing pulled from `request.url`'s search params; headers passed through unchanged. +- [x] `routes/api/incident-banner.ts`, `routes/api/incident-status.ts` — App + Router routes under `app/api/**` (already Web-native, direct re-export) + +--- + +## Compat shim surface (`compat/next/`) + +The Next compat shims stay alive as long as any `pages/...` file is +load-bearing. Listed here so the cleanup PR knows what to delete / +inline. + +- `router.ts` — `useRouter()` for hook callers (TanStack `useRouter` + + `useLocation` + `useMatches` + `useParams` + `useSearch` glued + together), plus a `default` export (`SingletonRouter` shape) for the + one module-scope `import router from 'next/router'` consumer + (Support/DiscordCTACard) that reads `router.basePath` outside React. + `router.pathname` strips the trailing slash TanStack appends to index + routes (without it, `router.pathname.split('/')[3]` returns `''` + instead of `undefined` for index pages and the project sidebar's + active-route check breaks). +- `_router-events.ts` — adapts `router.events.on(event, handler)` onto + `router.subscribe(tsEvent, …)`. Forwards Next's `(url, { shallow })` + args. Maps `routeChangeStart` / `routeChangeComplete` / + `beforeHistoryChange` / `hashChangeStart` / `hashChangeComplete`. + **Known gap:** Next's throw-from-`routeChangeStart`-to-cancel pattern + isn't supportable — `subscribe` is fire-and-forget. + `usePreventNavigationOnUnsavedChanges` relies on it and needs + migrating to TanStack's `useBlocker` separately. +- `api.ts` — `toWebHandler(nextHandler)`. See **API routes → Shim + coverage** above. +- `link.tsx`, `navigation.ts`, `dynamic.tsx`, `image.tsx`, + `legacy/image.tsx`, `script.tsx`, `head.tsx`, `server.ts` — comprehensive + drop-in replacements for the `next/*` modules studio imports. All + bundled via `vite.config.ts`'s `nextCompat()` plugin (alias) + + `ssr.noExternal: [/^next(\/|$)/]` so the shims always win over the + real Next packages. + +--- + +## Build / bundler workarounds + +`vite.config.ts` carries two classes of build-time guard that exist +purely because of how Rolldown chunks our specific dependency graph. +They should be revisited (and ideally lifted) once the migration is +done. + +### `manualChunks` pins + +Pin shared library code into dedicated chunks so per-component chunks +can't import from a chunk that (transitively) imports them back — +chunk-level cycles surface in the browser as +`TypeError: is not a function` at module-load time. + +- `class-variance-authority` — entry #1 in CIRCULAR_IMPORTS.md. +- `lucide-react` — keeps Lucide icons from being per-icon-split into + chunks that import `createLucideIcon` back from the `ui` chunk + (`folder-open-.js` was the canary). +- `react-vendor` (react + react-dom + scheduler + jsx-runtime) — pinned + before `lucide-react` so Rolldown doesn't suck React into the + lucide chunk for CJS interop and shift live-bindings across the rest + of the graph (`Alert-.js` was the canary). + +All three are documented in `CIRCULAR_IMPORTS.md` — slated for a +follow-up structural fix in `packages/ui` so the pins can be lifted. + +### `assertNoChunkCycles` build plugin + +Vite plugin that runs Tarjan's SCC on the emitted chunk graph in +`generateBundle` and fails the build if any unknown chunk cycle exists. +The pre-existing CVA cycle is allowlisted by chunk basename +(`KNOWN_CHUNK_CYCLES` constant) so the build still passes; any **new** +cycle blocks the build with a message pointing at CIRCULAR_IMPORTS.md. + +Keep this plugin even after migration — it's not a Next-related shim, +it's general protection against this entire class of bug. Just clear +the allowlist when the underlying cycle is gone. + +### Other build-side migration changes + +- `pnpm-workspace.yaml` catalog now includes `@tanstack/react-router`, + `@tanstack/react-start`, `@tanstack/react-table` so studio and + ui-library stay aligned. `react-query` is **not** in the catalog yet + — three consumers (studio, docs, ui-library) sit on different 5.x + ranges and unifying them is a separate decision. +- `NODE_OPTIONS=--max-old-space-size=8192` is set on the studio + `dev` script — Vite's Rolldown-RC frontend hits the default 4 GB + ceiling when chewing through studio's module graph in watch mode. + +--- + +## Deferred / revisit + +- ~~`pages/org/_/[[...routeSlug]].tsx`~~ landed as `routes/org.[_].tsx` + `routes/org.[_].$.tsx`. **Naming delta:** path-as-filename form (not `routes/org/[_]/index.tsx`) because the index-file form trips a router-generator bug at `getRouteNodes.js:132` — when an `index.tsx` has a bracket-escaped _parent_ segment, `originalRoutePath` gets wiped wholesale and the escape info is lost, so `_` gets stripped as pathless. The path-as-filename form keeps the last segment non-index and avoids the bug branch entirely. Next page accepts either Next-style `routeSlug` (string[]) or TanStack-style `_splat` (string) and normalises to the array shape. +- ~~`pages/project/_/[[...routeSlug]].tsx`~~ landed as `routes/project.[_].tsx` + `routes/project.[_].$.tsx`. Same naming-delta rationale as the org catch-alls above. + +### Cleanup checklist (after every `pages/...` file is gone) + +- Switch `routes/index.tsx` redirects from `href` to `to` — all targets + now live in the TanStack tree. +- Migrate `usePreventNavigationOnUnsavedChanges` from `router.events.on('routeChangeStart', …)` (throw-to-cancel pattern) to TanStack's `useBlocker`. +- Drop the `_splat` / `routeSlug` normalisation block from + `pages/org/_/[[...routeSlug]].tsx` + `pages/project/_/[[...routeSlug]].tsx` (only there to keep both runtimes mounting the same body). +- Remove `RouteValidationWrapper` + `next/router` compat shim usage from `__root.tsx`. +- Remove `compat/next/` directory entirely once no `next/*` import remains in workspace source. +- Lift `manualChunks` pins (`class-variance-authority`, `lucide-react`, `react-vendor`) once the structural fix in `packages/ui` lands — see CIRCULAR_IMPORTS.md. Keep `assertNoChunkCycles`; just clear `KNOWN_CHUNK_CYCLES`. +- Delete `pages/_app.tsx`, `pages/_document.tsx`, `pages/_error.jsx`, `pages/500.tsx`, `pages/404.tsx` (Next-only catch-alls; TanStack equivalents on `__root.tsx`). +- Drop the `dev:next` / `build:next` / `start:next` scripts from `apps/studio/package.json` once we're committed to TanStack. +- Delete this file. diff --git a/apps/studio/components/grid/components/common/MonacoEditor.tsx b/apps/studio/components/grid/components/common/MonacoEditor.tsx deleted file mode 100644 index d5e8c0dc827..00000000000 --- a/apps/studio/components/grid/components/common/MonacoEditor.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import Editor from '@monaco-editor/react' - -type MonacoEditorProps = { - width?: string | number | undefined - height?: string | number | undefined - value?: string | undefined - language?: string | undefined - readOnly?: boolean - onChange: (value: string | undefined) => void - onMount?: (editor: any) => void -} - -export const MonacoEditor = ({ - width, - height, - value, - language, - readOnly = false, - onChange, - onMount, -}: MonacoEditorProps) => { - function handleEditorOnMount(editor: any) { - // add margin above first line - editor.changeViewZones((accessor: any) => { - accessor.addZone({ - afterLineNumber: 0, - heightInPx: 4, - domNode: document.createElement('div'), - }) - }) - - // move cursor to the end of document - const model = editor.getModel() - const position = model.getPositionAt(value?.length) - editor.setPosition(position) - - // auto focus on mount - setTimeout(() => { - editor?.focus() - }, 0) - - if (onMount) onMount(editor) - } - - return ( - - ) -} diff --git a/apps/studio/components/grid/components/editor/JsonEditor.tsx b/apps/studio/components/grid/components/editor/JsonEditor.tsx index 1c9012cfadb..21c6224df7a 100644 --- a/apps/studio/components/grid/components/editor/JsonEditor.tsx +++ b/apps/studio/components/grid/components/editor/JsonEditor.tsx @@ -13,11 +13,11 @@ import { } from 'ui' import { BlockKeys } from '../common/BlockKeys' -import { MonacoEditor } from '../common/MonacoEditor' import { NullValue } from '../common/NullValue' import { TruncatedWarningOverlay } from './TruncatedWarningOverlay' import { useIsQueueOperationsEnabled } from '@/components/interfaces/Account/Preferences/useDashboardSettings' import { isValueTruncated } from '@/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils' +import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' import { useTableEditorQuery } from '@/data/table-editor/table-editor-query' import { isTableLike } from '@/data/table-editor/table-editor-types' import { useGetCellValueMutation } from '@/data/table-rows/get-cell-value-mutation' @@ -174,16 +174,24 @@ export const JsonEditor = ({ {isTruncated && !isSuccess ? (
- {}} value={value ?? ''} language="markdown" /> +
) : ( -
{isEditable && ( diff --git a/apps/studio/components/grid/components/editor/TextEditor.tsx b/apps/studio/components/grid/components/editor/TextEditor.tsx index 59f37c33a1b..a3931a74654 100644 --- a/apps/studio/components/grid/components/editor/TextEditor.tsx +++ b/apps/studio/components/grid/components/editor/TextEditor.tsx @@ -17,11 +17,11 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { BlockKeys } from '../common/BlockKeys' import { EmptyValue } from '../common/EmptyValue' -import { MonacoEditor } from '../common/MonacoEditor' import { NullValue } from '../common/NullValue' import { TruncatedWarningOverlay } from './TruncatedWarningOverlay' import { useTableRowOperations } from '@/components/grid/hooks/useTableRowOperations' import { isValueTruncated } from '@/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils' +import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' import { useTableEditorQuery } from '@/data/table-editor/table-editor-query' import { isTableLike } from '@/data/table-editor/table-editor-types' import { useGetCellValueMutation } from '@/data/table-rows/get-cell-value-mutation' @@ -129,7 +129,13 @@ export const TextEditor = ({ {isTruncated && !isSuccess ? (
- {}} value={value ?? ''} language="markdown" /> +
) : ( @@ -139,7 +145,15 @@ export const TextEditor = ({ onEnter={saveChanges} ignoreOutsideClicks={isConfirmNextModalOpen} > - + + {isEditable && (
diff --git a/apps/studio/components/grid/components/grid/Grid.tsx b/apps/studio/components/grid/components/grid/Grid.tsx index 04cad32dd0b..d5ba2b094b7 100644 --- a/apps/studio/components/grid/components/grid/Grid.tsx +++ b/apps/studio/components/grid/components/grid/Grid.tsx @@ -89,11 +89,12 @@ export const Grid = memo( const tableEntityType = snap.originalTable?.entity_type const isForeignTable = tableEntityType === ENTITY_TYPE.FOREIGN_TABLE const isTableEmpty = (rows ?? []).length === 0 + const canImportData = snap.editable && !isForeignTable const track = useTrack() const { isDraggedOver, onDragOver, onFileDrop } = useCsvFileDrop({ - enabled: isTableEmpty && !isForeignTable, + enabled: isTableEmpty && canImportData, onFileDropped: (file) => tableEditorSnap.onImportData(valtioRef(file)), onTelemetryEvent: (eventName) => track(eventName), }) @@ -322,7 +323,7 @@ export const Grid = memo( started.

- ) : ( + ) : canImportData ? (
- )} + ) : null}
) : (
diff --git a/apps/studio/components/interfaces/App/FeaturePreview/InlineEditorPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/InlineEditorPreview.tsx deleted file mode 100644 index 7b9beaa3d26..00000000000 --- a/apps/studio/components/interfaces/App/FeaturePreview/InlineEditorPreview.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import Image from 'next/image' - -import { BASE_PATH } from '@/lib/constants' - -export const InlineEditorPreview = () => { - return ( -
- -

- Edit policies, functions, and triggers directly in the inline SQL editor. When you select - any of these database objects, the editor opens automatically, allowing you to make changes - without switching contexts. -

-

- Need help writing SQL? Use the inline Assistant to generate or modify code for your - policies, triggers, and functions without leaving the editor. -

-

- Access the inline editor anytime by clicking the code editor icon in the top right corner of - your dashboard. -

-
- ) -} diff --git a/apps/studio/components/interfaces/App/FeaturePreview/UnifiedLogsPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/UnifiedLogsPreview.tsx index 7448f63c2d8..67f08adce70 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/UnifiedLogsPreview.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/UnifiedLogsPreview.tsx @@ -13,10 +13,6 @@ export const UnifiedLogsPreview = () => { Experience our enhanced Logs interface with improved filtering, real-time updates, and a unified view across all your services. Built for better performance and easier debugging.

-

- This interface is only available for organizations on the Enterprise plan. -

- { isNew: false, isPlatformOnly: true, isDefaultOptIn: false, - getRoute: (ref?: string) => `/project/${ref}/advisors/rules`, + getRoute: (ref?: string) => `/project/${ref}/advisors/rules/security`, }, { diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.constants.ts b/apps/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.constants.ts deleted file mode 100644 index a4e1318751e..00000000000 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/AuthProvidersForm.constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const ProviderCollapsibleClasses = [ - 'bg-surface-100', - 'hover:bg-overlay-hover', - 'data-open:bg-selection', - 'border-default', - 'hover:border-strong data-open:border-strong', - 'data-open:pb-px col-span-12 mx-auto', - '-space-y-px overflow-hidden', - 'border shadow-sm', - 'transition', - 'first:rounded-tl', - 'first:rounded-tr', - 'last:rounded-bl', - 'last:rounded-br', - 'hover:z-50', -] diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx index 8a2a43b66b2..b24311c00da 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx @@ -365,10 +365,16 @@ export const TemplateEditor = ({ template, isReadOnly = false }: TemplateEditorP

Template variables

Data placeholders that can be inserted into the subject or body.{' '} + + Terminology + + {' · '} - Learn more + Local development

diff --git a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx index 24055a99f51..c1dbe06022e 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx @@ -35,10 +35,10 @@ import { Hook, HOOK_DEFINITION_TITLE, HOOKS_DEFINITIONS } from './hooks.constant import { extractMethod, getRevokePermissionStatements, isValidHook } from './hooks.utils' import { convertArgumentTypes } from '@/components/interfaces/Database/Functions/Functions.utils' import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' -import CodeEditor from '@/components/ui/CodeEditor/CodeEditor' +import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' import { DocsButton } from '@/components/ui/DocsButton' import FunctionSelector from '@/components/ui/FunctionSelector' -import SchemaSelector from '@/components/ui/SchemaSelector' +import { SchemaSelector } from '@/components/ui/SchemaSelector' import { AuthConfigResponse } from '@/data/auth/auth-config-query' import { useAuthHooksUpdateMutation } from '@/data/auth/auth-hooks-update-mutation' import { executeSql } from '@/data/sql/execute-sql-mutation' diff --git a/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx b/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx index 8f0ad3fa09a..a40b77793d1 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/HooksListing.tsx @@ -20,8 +20,8 @@ import { CreateHookSheet } from './CreateHookSheet' import { HookCard } from './HookCard' import { Hook, HOOKS_DEFINITIONS } from './hooks.constants' import { extractMethod, getRevokePermissionStatements, isValidHook } from './hooks.utils' -import AlertError from '@/components/ui/AlertError' -import CodeEditor from '@/components/ui/CodeEditor/CodeEditor' +import { AlertError } from '@/components/ui/AlertError' +import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' import { useAuthConfigQuery } from '@/data/auth/auth-config-query' import { useAuthHooksUpdateMutation } from '@/data/auth/auth-hooks-update-mutation' import { executeSql } from '@/data/sql/execute-sql-mutation' diff --git a/apps/studio/components/interfaces/Auth/RLSTester/InferredSQLViewer.tsx b/apps/studio/components/interfaces/Auth/RLSTester/InferredSQLViewer.tsx index 7174f586891..ecdaf4b6b80 100644 --- a/apps/studio/components/interfaces/Auth/RLSTester/InferredSQLViewer.tsx +++ b/apps/studio/components/interfaces/Auth/RLSTester/InferredSQLViewer.tsx @@ -2,7 +2,7 @@ import { UntrustedSqlFragment } from '@supabase/pg-meta' import { Loader2 } from 'lucide-react' import { Badge, Tooltip, TooltipContent, TooltipTrigger } from 'ui' -import CodeEditor from '@/components/ui/CodeEditor/CodeEditor' +import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' export const InferredSQLViewer = ({ sql, diff --git a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx deleted file mode 100644 index cbf2058174b..00000000000 --- a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/CurrentPaymentMethod.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { PermissionAction, SupportCategories } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' -import { CreditCardIcon } from 'lucide-react' -import Link from 'next/link' -import { Button } from 'ui' -import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' - -import CreditCard from './CreditCard' -import { SupportLink } from '@/components/interfaces/Support/SupportLink' -import { useOrganizationPaymentMethodsQuery } from '@/data/organizations/organization-payment-methods-query' -import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' -import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' - -const CurrentPaymentMethod = () => { - const { slug } = useParams() - - const { - data: subscription, - isPending: isLoadingSubscription, - isError: isErrorSubscription, - } = useOrgSubscriptionQuery({ - orgSlug: slug, - }) - const { - data: paymentMethods, - isPending: isLoadingOrganizationPaymentMethods, - isError: isErrorOrganizationPaymentMethods, - } = useOrganizationPaymentMethodsQuery({ slug }) - - const isLoading = isLoadingSubscription || isLoadingOrganizationPaymentMethods - const isError = isErrorSubscription || isErrorOrganizationPaymentMethods - - const defaultPaymentMethod = paymentMethods?.data.find((pm) => pm.is_default) - - const { can: canReadPaymentMethods } = useAsyncCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.payment_methods' - ) - - // since this component is an enhancement, - // if it can't read payment methods, we will just not show it - if (!canReadPaymentMethods || isError) return null - - return ( -
- {isLoading ? ( - - ) : subscription?.payment_method_type === 'invoice' ? ( -

- You get a monthly invoice and payment link via email. To change your payment method, - please contact us via our support form. -

- ) : !defaultPaymentMethod ? ( -
- -

No payment methods

-
- ) : ( - - )} - - -
- ) -} - -export default CurrentPaymentMethod diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index 0b7e479e88c..95cdf11692b 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -93,8 +93,8 @@ export const CreateBranchModal = () => { .string() .min(1, 'Branch name cannot be empty') .refine( - (val) => /^[a-zA-Z0-9\-_]+$/.test(val), - 'Branch name can only contain alphanumeric characters, hyphens, and underscores.' + (val) => /^[a-zA-Z0-9\-_/]+$/.test(val), + 'Only letters, numbers, hyphens, underscores, and forward slashes are allowed.' ) .refine( (val) => (branches ?? []).every((branch) => branch.name !== val), @@ -105,7 +105,7 @@ export const CreateBranchModal = () => { }) const form = useForm>({ - mode: 'onSubmit', + mode: 'onChange', reValidateMode: 'onBlur', resolver: zodResolver(FormSchema), defaultValues: { branchName: '', gitBranchName: '', withData: false }, @@ -307,7 +307,7 @@ export const CreateBranchModal = () => { control={form.control} name="branchName" render={({ field }) => ( - + -
-

{label}

-
-
- -
-
- ) -} - -export function ApiKeysTabContent({ projectKeys }: { projectKeys: projectKeys }) { - const { ref: projectRef } = useParams() - - const { isLoading: isLoadingPermissions, can: canReadAPIKeys } = useAsyncCheckPermissions( - PermissionAction.SECRETS_READ, - '*' - ) - - if (isLoadingPermissions) { - return ( -
- -

Retrieving API keys

-
- ) - } - - if (!canReadAPIKeys) { - return ( -
- -

You don't have permission to view API keys.

-
- ) - } - - return ( -
- - - - - - Anon Key (Legacy) - - } - value={projectKeys.anonKey ?? ''} - /> - -
-
-

For secret keys, see API settings.

- -
-
-
- ) -} diff --git a/apps/studio/components/interfaces/Connect/Connect.constants.ts b/apps/studio/components/interfaces/Connect/Connect.constants.ts deleted file mode 100644 index 6b71f1e8229..00000000000 --- a/apps/studio/components/interfaces/Connect/Connect.constants.ts +++ /dev/null @@ -1,414 +0,0 @@ -import { type CodeBlockLang } from 'ui-patterns/CodeBlock' - -import { DOCS_URL } from '@/lib/constants' - -export type DatabaseConnectionType = - | 'uri' - | 'psql' - | 'golang' - | 'jdbc' - | 'dotnet' - | 'nodejs' - | 'php' - | 'python' - | 'sqlalchemy' - -export const DATABASE_CONNECTION_TYPES: { - id: DatabaseConnectionType - label: string - contentType: 'input' | 'code' - lang: CodeBlockLang - fileTitle: string | undefined -}[] = [ - { id: 'uri', label: 'URI', contentType: 'input', lang: 'bash', fileTitle: undefined }, - { id: 'psql', label: 'PSQL', contentType: 'code', lang: 'bash', fileTitle: undefined }, - { id: 'golang', label: 'Golang', contentType: 'code', lang: 'go', fileTitle: '.env' }, - { id: 'jdbc', label: 'JDBC', contentType: 'input', lang: 'bash', fileTitle: undefined }, - { - id: 'dotnet', - label: '.NET', - contentType: 'code', - lang: 'csharp', - fileTitle: 'appsettings.json', - }, - { id: 'nodejs', label: 'Node.js', contentType: 'code', lang: 'js', fileTitle: '.env' }, - { id: 'php', label: 'PHP', contentType: 'code', lang: 'php', fileTitle: '.env' }, - { id: 'python', label: 'Python', contentType: 'code', lang: 'python', fileTitle: '.env' }, - { id: 'sqlalchemy', label: 'SQLAlchemy', contentType: 'code', lang: 'python', fileTitle: '.env' }, -] - -export const CONNECTION_PARAMETERS = { - host: { - key: 'host', - description: 'The hostname of your database', - }, - port: { - key: 'port', - description: 'Port number for the connection', - }, - database: { - key: 'database', - description: 'Default database name', - }, - user: { - key: 'user', - description: 'Database user', - }, - pool_mode: { - key: 'pool_mode', - description: 'Connection pooling behavior', - }, -} as const - -export type ConnectionType = { - key: string - icon: string - label: string - guideLink?: string - children: ConnectionType[] - files?: { - name: string - content: string - }[] -} - -export const FRAMEWORKS: ConnectionType[] = [ - { - key: 'nextjs', - label: 'Next.js', - icon: 'nextjs', - guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/nextjs`, - children: [ - { - key: 'app', - label: 'App Router', - icon: '', - children: [ - { - key: 'supabasejs', - label: 'supabase-js', - icon: 'supabase', - children: [], - }, - ], - }, - { - key: 'pages', - label: 'Pages Router', - icon: '', - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - ], - }, - { - key: 'remix', - label: 'React Router', - icon: 'remix', - guideLink: `${DOCS_URL}/guides/auth/server-side/creating-a-client?framework=remix&environment=remix-loader`, - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'react', - label: 'React', - icon: 'react', - guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/reactjs`, - children: [ - { - key: 'vite', - label: 'Vite', - icon: 'vite', - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'create-react-app', - label: 'Create React App', - icon: 'react', - children: [ - { - key: 'supabasejs', - label: 'supabase-js', - icon: 'supabase', - children: [], - }, - ], - }, - ], - }, - { - key: 'nuxt', - label: 'Nuxt', - icon: 'nuxt', - guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/nuxtjs`, - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'vuejs', - label: 'Vue.JS', - icon: 'vuejs', - guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/vue`, - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - - { - key: 'sveltekit', - label: 'SvelteKit', - icon: 'sveltekit', - guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/sveltekit`, - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'solidjs', - label: 'Solid.js', - icon: 'solidjs', - guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/solidjs`, - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'astro', - label: 'Astro', - icon: 'astro', - guideLink: 'https://docs.astro.build/en/guides/backend/supabase/', - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'refine', - label: 'Refine', - icon: 'refine', - guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/refine`, - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'tanstack', - label: 'TanStack Start', - icon: 'tanstack', - guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/tanstack`, - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'flask', - label: 'Flask (Python)', - icon: 'python', - guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/flask`, - children: [ - { - key: 'supabasepy', - label: 'supabase-py', - children: [], - icon: 'supabase', - }, - ], - }, -] - -export const MOBILES: ConnectionType[] = [ - { - key: 'exporeactnative', - label: 'Expo React Native', - icon: 'expo', - guideLink: `${DOCS_URL}/guides/getting-started/quickstarts/expo-react-native`, - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'flutter', - label: 'Flutter', - icon: 'flutter', - guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-flutter`, - children: [ - { - key: 'supabaseflutter', - label: 'supabase-flutter', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'ionicreact', - label: 'Ionic React', - icon: 'react', - guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-ionic-react`, - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'swift', - label: 'Swift', - icon: 'swift', - guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-swift`, - children: [ - { - key: 'supabaseswift', - label: 'supabase-swift', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'androidkotlin', - label: 'Android Kotlin', - icon: 'kotlin', - guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-kotlin`, - children: [ - { - key: 'supabasekt', - label: 'supabase-kt', - children: [], - icon: 'supabase', - }, - ], - }, - { - key: 'ionicangular', - label: 'Ionic Angular', - icon: 'ionic-angular', - guideLink: `${DOCS_URL}/guides/getting-started/tutorials/with-ionic-angular`, - children: [ - { - key: 'supabasejs', - label: 'Supabase-js', - children: [], - icon: 'supabase', - }, - ], - }, -] - -export const ORMS: ConnectionType[] = [ - { - key: 'prisma', - label: 'Prisma', - icon: 'prisma', - guideLink: 'https://supabase.com/partners/integrations/prisma', - children: [], - }, - { - key: 'drizzle', - label: 'Drizzle', - icon: 'drizzle', - guideLink: `${DOCS_URL}/guides/database/connecting-to-postgres#connecting-with-drizzle`, - children: [], - }, -] - -export const CONNECTION_TYPES = [ - { key: 'direct', label: 'Connection String', obj: [] }, - { key: 'frameworks', label: 'App Frameworks', obj: FRAMEWORKS }, - { key: 'mobiles', label: 'Mobile Frameworks', obj: MOBILES }, - { key: 'orms', label: 'ORMs', obj: ORMS }, - { key: 'api-keys', label: 'API Keys', obj: [] }, - { key: 'mcp', label: 'MCP', obj: [] }, -] - -export const PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT = - 'Purchase IPv4 add-on or use Shared Pooler if on a IPv4 network' -export const IPV4_ADDON_TEXT = 'Connections are IPv4 proxied with IPv4 add-on' - -export type ConnectionStringMethod = 'direct' | 'transaction' | 'session' - -export const connectionStringMethodOptions: Record< - ConnectionStringMethod, - { value: string; label: string; description: string } -> = { - direct: { - value: 'direct', - label: 'Direct connection', - description: - 'Ideal for applications with persistent and long-lived connections, such as those running on virtual machines or long-standing containers.', - }, - transaction: { - value: 'transaction', - label: 'Transaction pooler', - description: - 'Ideal for stateless applications like serverless functions where each interaction with Postgres is brief and isolated.', - }, - session: { - value: 'session', - label: 'Session pooler', - description: - 'Only recommended as an alternative to Direct Connection, when connecting via an IPv4 network.', - }, -} diff --git a/apps/studio/components/interfaces/Connect/Connect.types.ts b/apps/studio/components/interfaces/Connect/Connect.types.ts deleted file mode 100644 index d75a5df614d..00000000000 --- a/apps/studio/components/interfaces/Connect/Connect.types.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type projectKeys = { - apiUrl: string | null - anonKey: string | null - publishableKey: string | null -} - -export interface ContentFileProps { - projectKeys: { - apiUrl: string - anonKey?: string - publishableKey?: string - } - connectionStringPooler: { - transactionShared: string - sessionShared: string - transactionDedicated?: string - sessionDedicated?: string - ipv4SupportedForDedicatedPooler: boolean - direct?: string - } - connectionTab: 'App Frameworks' | 'Mobile Frameworks' | 'ORMs' - onCopy?: () => void -} diff --git a/apps/studio/components/interfaces/Connect/Connect.utils.ts b/apps/studio/components/interfaces/Connect/Connect.utils.ts deleted file mode 100644 index 885604d1896..00000000000 --- a/apps/studio/components/interfaces/Connect/Connect.utils.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ConnectionType, FRAMEWORKS, MOBILES, ORMS } from './Connect.constants' - -export function getProjectRef(url: string): string | null { - const regex: RegExp = /https:\/\/([^\.]+)\./ - const match: RegExpMatchArray | null = url.match(regex) - - if (match) { - return match[1] - } else { - return null - } -} - -export const getContentFilePath = ({ - connectionObject, - selectedParent, - selectedChild, - selectedGrandchild, -}: { - selectedParent: string - selectedChild: string - selectedGrandchild: string - connectionObject: ConnectionType[] -}) => { - const parent = connectionObject.find((item) => item.key === selectedParent) - - if (parent) { - const child = parent.children.find((child) => child.key === selectedChild) - - // check grandchild first, then child, then parent as the fallback - if (child) { - const grandchild = child.children.find((grandchild) => grandchild.key === selectedGrandchild) - - if (grandchild) { - return `${selectedParent}/${selectedChild}/${selectedGrandchild}` - } else { - return `${selectedParent}/${selectedChild}` - } - } else { - return selectedParent - } - } - - return '' -} - -export function inferConnectTabFromParentKey( - parentKey: string | null -): 'frameworks' | 'mobiles' | 'orms' | null { - if (!parentKey) return null - if (FRAMEWORKS.find((x: ConnectionType) => x.key === parentKey)) return 'frameworks' - if (MOBILES.find((x: ConnectionType) => x.key === parentKey)) return 'mobiles' - if (ORMS.find((x: ConnectionType) => x.key === parentKey)) return 'orms' - return null -} diff --git a/apps/studio/components/interfaces/Connect/ConnectDropdown.tsx b/apps/studio/components/interfaces/Connect/ConnectDropdown.tsx deleted file mode 100644 index e56f50097a8..00000000000 --- a/apps/studio/components/interfaces/Connect/ConnectDropdown.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Box, Check, ChevronDown } from 'lucide-react' -import { useState } from 'react' -import { - Button, - cn, - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - Popover, - PopoverContent, - PopoverTrigger, -} from 'ui' - -import { ConnectionType } from './Connect.constants' -import { ConnectionIcon } from './ConnectionIcon' - -interface ConnectDropdownProps { - state: string - updateState: (state: string) => void - label: string - items: ConnectionType[] - iconFolder?: string - supportsDarkMode?: boolean -} - -export const ConnectDropdown = ({ - state, - updateState, - label, - - items, - iconFolder, - supportsDarkMode, -}: ConnectDropdownProps) => { - const [open, setOpen] = useState(false) - - function onSelectLib(key: string) { - updateState(key) - setOpen(false) - } - - const selectedItem = items.find((item) => item.key === state) - - return ( - -
- - {label} - - - - -
- - - - - No results found. - - {items.map((item) => ( - { - onSelectLib(item.key) - setOpen(false) - }} - className="flex gap-2 items-center" - > - {item.icon ? ( - - ) : ( - - )} - {item.label} - - - ))} - - - - -
- ) -} diff --git a/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx b/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx deleted file mode 100644 index e3ba0beda3f..00000000000 --- a/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { useParams } from 'common' -import dynamic from 'next/dynamic' -import { forwardRef, HTMLAttributes, useMemo } from 'react' -import { cn } from 'ui' -import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' -import { CopyCallbackContext } from 'ui-patterns/SimpleCodeBlock' - -import { getAddons } from '../Billing/Subscription/Subscription.utils' -import type { projectKeys } from './Connect.types' -import { getConnectionStrings } from './DatabaseSettings.utils' -import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' -import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query' -import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query' -import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query' -import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' -import { pluckObjectFields } from '@/lib/helpers' -import { useTrack } from '@/lib/telemetry/track' - -interface ConnectContentTabProps extends HTMLAttributes { - projectKeys: projectKeys - filePath: string - connectionTab: 'App Frameworks' | 'Mobile Frameworks' | 'ORMs' - selectedFrameworkOrTool: string - connectionStringPooler?: { - transactionShared: string - sessionShared: string - transactionDedicated?: string - sessionDedicated?: string - ipv4SupportedForDedicatedPooler: boolean - direct?: string - } -} - -export const ConnectTabContent = forwardRef( - ({ projectKeys, filePath, connectionTab, selectedFrameworkOrTool, ...props }, ref) => { - const { ref: projectRef } = useParams() - const track = useTrack() - const { hasAccess: allowPgBouncerSelection } = useCheckEntitlements('dedicated_pooler') - - const handleCopy = () => { - const trackingProperties: { - connectionTab: 'App Frameworks' | 'Mobile Frameworks' | 'ORMs' - selectedItem: string - connectionType?: string - lang?: string - } = { - connectionTab, - selectedItem: selectedFrameworkOrTool, - } - - // Only include connectionType and lang for App Frameworks and Mobile Frameworks - if (connectionTab !== 'ORMs') { - trackingProperties.connectionType = 'Framework snippet' - trackingProperties.lang = filePath.split('/').pop() ?? 'unknown' - } - - track('connection_string_copied', trackingProperties) - } - - const { data: settings } = useProjectSettingsV2Query({ projectRef }) - const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef }) - const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef }) - const { data: addons } = useProjectAddonsQuery({ projectRef }) - const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? []) - - const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at'] - const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' } - const connectionInfo = pluckObjectFields(settings || emptyState, DB_FIELDS) - const poolingConfigurationShared = supavisorConfig?.find((x) => x.database_type === 'PRIMARY') - const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined - - const connectionStringsShared = getConnectionStrings({ - connectionInfo, - poolingInfo: { - connectionString: poolingConfigurationShared?.connection_string ?? '', - db_host: poolingConfigurationShared?.db_host ?? '', - db_name: poolingConfigurationShared?.db_name ?? '', - db_port: poolingConfigurationShared?.db_port ?? 0, - db_user: poolingConfigurationShared?.db_user ?? '', - }, - metadata: { projectRef }, - }) - - const connectionStringsDedicated = - poolingConfigurationDedicated !== undefined - ? getConnectionStrings({ - connectionInfo, - poolingInfo: { - connectionString: poolingConfigurationDedicated.connection_string, - db_host: poolingConfigurationDedicated.db_host, - db_name: poolingConfigurationDedicated.db_name, - db_port: poolingConfigurationDedicated.db_port, - db_user: poolingConfigurationDedicated.db_user, - }, - metadata: { projectRef }, - }) - : undefined - - const ContentFile = useMemo(() => { - return dynamic(() => import(`./content/${filePath}/content`), { - loading: () => ( -
- -
- ), - }) - }, [filePath]) - - return ( -
- - - -
- ) - } -) - -ConnectTabContent.displayName = 'ConnectTabContent' diff --git a/apps/studio/components/interfaces/Connect/ConnectTabs.tsx b/apps/studio/components/interfaces/Connect/ConnectTabs.tsx deleted file mode 100644 index 791851c3b8e..00000000000 --- a/apps/studio/components/interfaces/Connect/ConnectTabs.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { FileJson2 } from 'lucide-react' -import { isValidElement, ReactNode } from 'react' -import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui' - -interface ConnectTabTriggerProps { - value: string -} -interface ConnectTabTriggersProps { - children: ReactNode[] -} - -interface ConnectFileTabProps { - children: ReactNode[] - value?: string - onValueChange?: (value: string) => void -} - -interface ConnectTabContentProps { - children: ReactNode - value: string -} -export const ConnectTabs = ({ children, value, onValueChange }: ConnectFileTabProps) => { - const firstChild = children[0] - - const defaultValue = isValidElement(firstChild) - ? (firstChild.props as any)?.children[0]?.props?.value || '' - : null - - return ( - - {children} - - ) -} - -export const ConnectTabTrigger = ({ value }: ConnectTabTriggerProps) => { - return ( - - - {value} - - ) -} - -export const ConnectTabTriggers = ({ children }: ConnectTabTriggersProps) => { - return ( - - {children} - - ) -} - -export const ConnectTabContent = ({ value, children }: ConnectTabContentProps) => { - return ( - - {children} - - ) -} diff --git a/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx b/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx deleted file mode 100644 index acbffe37825..00000000000 --- a/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import { useParams } from 'common' -import { ChevronRight, FileCode, X } from 'lucide-react' -import Link from 'next/link' -import { PropsWithChildren, ReactNode } from 'react' -import { - Badge, - Button, - cn, - Collapsible, - CollapsibleContent, - CollapsibleTrigger, - WarningIcon, -} from 'ui' -import { Admonition } from 'ui-patterns/admonition' -import { CodeBlock, type CodeBlockLang } from 'ui-patterns/CodeBlock' - -import { ConnectionParameters } from './ConnectionParameters' -import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query' -import { IS_PLATFORM } from '@/lib/constants' -import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' - -interface ConnectionPanelProps { - type?: 'direct' | 'transaction' | 'session' - badge?: string - title: string - description: string - contentFooter?: ReactNode - connectionString: string - ipv4Status: { - type: 'error' | 'success' - title: string - description?: string | ReactNode - links?: { text: string; url: string }[] - } - notice?: string[] - parameters?: Array<{ - key: string - value: string - description?: string - }> - contentType?: 'input' | 'code' - lang?: CodeBlockLang - fileTitle?: string - onCopyCallback: () => void -} - -const IPv4StatusIcon = ({ className, active }: { className?: string; active: boolean }) => { - return ( -
- - - - - {!active ? ( -
- -
- ) : ( -
- - - -
- )} -
- ) -} - -export const CodeBlockFileHeader = ({ title }: { title: string }) => { - return ( -
-
- - {title} -
-
- ) -} - -export const ConnectionPanel = ({ - type = 'direct', - badge, - title, - description, - contentFooter, - connectionString, - ipv4Status, - notice, - parameters = [], - lang = 'bash', - fileTitle, - children, - onCopyCallback, -}: PropsWithChildren) => { - const { ref: projectRef } = useParams() - const state = useDatabaseSelectorStateSnapshot() - - const { data: poolingInfo } = useSupavisorConfigurationQuery({ projectRef }) - const poolingConfiguration = poolingInfo?.find((x) => x.identifier === state.selectedDatabaseId) - const isSessionMode = poolingConfiguration?.pool_mode === 'session' - - const links = ipv4Status.links ?? [] - - const isTransactionDedicatedPooler = type === 'transaction' && badge === 'Dedicated Pooler' - - return ( -
-
-
-

{title}

- {!!badge && !isTransactionDedicatedPooler && {badge}} -
-

{description}

- {contentFooter} -
-
- {isTransactionDedicatedPooler && ( -
- Using the Dedicated Pooler: -
- )} -
- {fileTitle && } - {type === 'transaction' && isSessionMode ? ( - - - - ) : ( - <> - - {notice && ( -
- {notice?.map((text: string) => ( -

- {text} -

- ))} -
- )} - {parameters.length > 0 && } - - )} -
-
- {IS_PLATFORM && ( -
-
- -
-
- {ipv4Status.title} - {ipv4Status.description && - (typeof ipv4Status.description === 'string' ? ( - - {ipv4Status.description} - - ) : ( - ipv4Status.description - ))} - {links.length > 0 && ( -
- {links.map((link) => ( - - ))} -
- )} -
-
- )} - - {type === 'session' && ( -
-
- -
-
- Only use on a IPv4 network -
-

Session pooler connections are IPv4 proxied for free.

-

Use Direct Connection if connecting via an IPv6 network.

-
-
-
- )} - - {IS_PLATFORM && ipv4Status.type === 'error' && ( - - - - - -
-

- A few major platforms are IPv4-only and may not work with a Direct Connection: -

-
-
Vercel
-
GitHub Actions
-
Render
-
Retool
-
-

- If you wish to use a Direct Connection with these, please purchase{' '} - - IPv4 support - - . -

-

- You may also use the{' '} - Session Pooler or{' '} - Transaction Pooler if you are on - a IPv4 network. -

-
-
-
- )} -
- {children} -
-
- ) -} diff --git a/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx b/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx deleted file mode 100644 index 59233d7b4ed..00000000000 --- a/apps/studio/components/interfaces/Connect/ConnectionParameters.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Check, ChevronRight, Copy } from 'lucide-react' -import { useState } from 'react' -import { - Button, - cn, - Collapsible, - CollapsibleContent, - CollapsibleTrigger, - copyToClipboard, - Separator, -} from 'ui' - -interface Parameter { - key: string - value: string -} - -interface ConnectionParametersProps { - parameters: Parameter[] -} - -export const ConnectionParameters = ({ parameters }: ConnectionParametersProps) => { - const [isOpen, setIsOpen] = useState(false) - const [copiedMap, setCopiedMap] = useState>({}) - - return ( - - - - - -
- {parameters.map((param) => ( -
-
- {param.key}: - {param.value} - -
-
- ))} -
- -
- For security reasons, your database password is never shown. -
-
-
- ) -} diff --git a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx deleted file mode 100644 index 97949de5678..00000000000 --- a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx +++ /dev/null @@ -1,672 +0,0 @@ -import { useParams } from 'common' -import { BookOpen, ChevronDown, ExternalLink } from 'lucide-react' -import { parseAsString, useQueryState } from 'nuqs' -import { HTMLAttributes, ReactNode, useEffect, useState } from 'react' -import { - Badge, - Button, - cn, - Collapsible, - CollapsibleContent, - CollapsibleTrigger, - DIALOG_PADDING_X, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - Separator, -} from 'ui' -import { CodeBlock } from 'ui-patterns/CodeBlock' -import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' - -import { - CONNECTION_PARAMETERS, - connectionStringMethodOptions, - DATABASE_CONNECTION_TYPES, - DatabaseConnectionType, - IPV4_ADDON_TEXT, - PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT, - type ConnectionStringMethod, -} from './Connect.constants' -import { CodeBlockFileHeader, ConnectionPanel } from './ConnectionPanel' -import { getConnectionStrings } from './DatabaseSettings.utils' -import { examples, type Example } from './DirectConnectionExamples' -import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils' -import AlertError from '@/components/ui/AlertError' -import { DatabaseSelector } from '@/components/ui/DatabaseSelector' -import { InlineLink } from '@/components/ui/InlineLink' -import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query' -import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query' -import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' -import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query' -import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' -import { DOCS_URL, IS_PLATFORM } from '@/lib/constants' -import { pluckObjectFields } from '@/lib/helpers' -import { useTrack } from '@/lib/telemetry/track' -import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' - -const StepLabel = ({ - number, - children, - ...props -}: { number: number; children: ReactNode } & HTMLAttributes) => ( -
-
- {number} -
- {children} -
-) - -/** - * [Joshen] For paid projects - Dedicated pooler is always in transaction mode - * So session mode connection details are always using the shared pooler (Supavisor) - */ -export const DatabaseConnectionString = () => { - const { ref: projectRef } = useParams() - const state = useDatabaseSelectorStateSnapshot() - const { - hasAccess: hasDedicatedPooler, - isLoading: isLoadingEntitlement, - isSuccess: isSuccessEntitlement, - } = useCheckEntitlements('dedicated_pooler') - const sharedPoolerPreferred = !hasDedicatedPooler - - // URL state management - const [queryType, setQueryType] = useQueryState('type', parseAsString.withDefault('uri')) - const [querySource, setQuerySource] = useQueryState('source', parseAsString) - const [queryMethod, setQueryMethod] = useQueryState('method', parseAsString.withDefault('direct')) - - const [selectedTab, setSelectedTab] = useState('uri') - const [selectedMethod, setSelectedMethod] = useState('direct') - - // Sync URL state with component state on mount and when URL changes - useEffect(() => { - const validTypes = DATABASE_CONNECTION_TYPES.map((t) => t.id) - if (queryType && validTypes.includes(queryType as DatabaseConnectionType)) { - setSelectedTab(queryType as DatabaseConnectionType) - } else if (queryType && !validTypes.includes(queryType as DatabaseConnectionType)) { - setQueryType('uri') - setSelectedTab('uri') - } - - const validMethods: ConnectionStringMethod[] = ['direct', 'transaction', 'session'] - if (queryMethod && validMethods.includes(queryMethod as ConnectionStringMethod)) { - setSelectedMethod(queryMethod as ConnectionStringMethod) - } else if (queryMethod && !validMethods.includes(queryMethod as ConnectionStringMethod)) { - setQueryMethod('direct') - setSelectedMethod('direct') - } - - if (querySource && querySource !== state.selectedDatabaseId) { - state.setSelectedDatabaseId(querySource) - } else if (!querySource && state.selectedDatabaseId !== projectRef) { - state.setSelectedDatabaseId(projectRef) - } - }, [queryType, queryMethod, querySource, state]) - - // Sync component state changes back to URL - const handleTabChange = (connectionType: DatabaseConnectionType) => { - setSelectedTab(connectionType) - setQueryType(connectionType) - } - - const handleMethodChange = (method: ConnectionStringMethod) => { - setSelectedMethod(method) - setQueryMethod(method) - } - - const handleDatabaseChange = (databaseId: string) => { - if (databaseId === projectRef) { - setQuerySource(null) - } else { - setQuerySource(databaseId) - } - } - - // Sync database selector state changes back to URL - useEffect(() => { - if (state.selectedDatabaseId && state.selectedDatabaseId !== querySource) { - // Only set source in URL if it's not the primary database - if (state.selectedDatabaseId === projectRef) { - setQuerySource(null) - } else { - setQuerySource(state.selectedDatabaseId) - } - } - }, [state.selectedDatabaseId, querySource, projectRef]) - - const { - data: pgbouncerConfig, - error: pgbouncerError, - isPending: isLoadingPgbouncerConfig, - isError: isErrorPgbouncerConfig, - isSuccess: isSuccessPgBouncerConfig, - } = usePgbouncerConfigQuery({ projectRef }) - const { - data: supavisorConfig, - error: supavisorConfigError, - isPending: isLoadingSupavisorConfig, - isError: isErrorSupavisorConfig, - isSuccess: isSuccessSupavisorConfig, - } = useSupavisorConfigurationQuery({ projectRef }) - - const { - data: databases, - error: readReplicasError, - isPending: isLoadingReadReplicas, - isError: isErrorReadReplicas, - isSuccess: isSuccessReadReplicas, - } = useReadReplicasQuery({ projectRef }) - - const poolerError = sharedPoolerPreferred ? pgbouncerError : supavisorConfigError - const isLoadingPoolerConfig = !IS_PLATFORM - ? false - : sharedPoolerPreferred - ? isLoadingPgbouncerConfig - : isLoadingSupavisorConfig - const isErrorPoolerConfig = !IS_PLATFORM - ? undefined - : sharedPoolerPreferred - ? isErrorPgbouncerConfig - : isErrorSupavisorConfig - const isSuccessPoolerConfig = !IS_PLATFORM - ? true - : sharedPoolerPreferred - ? isSuccessPgBouncerConfig - : isSuccessSupavisorConfig - - const error = poolerError || readReplicasError - const isLoading = isLoadingPoolerConfig || isLoadingReadReplicas || isLoadingEntitlement - const isError = isErrorPoolerConfig || isErrorReadReplicas - const isSuccess = isSuccessPoolerConfig && isSuccessReadReplicas && isSuccessEntitlement - - const sharedPoolerConfig = supavisorConfig?.find((x) => x.identifier === state.selectedDatabaseId) - const poolingConfiguration = sharedPoolerPreferred ? sharedPoolerConfig : pgbouncerConfig - - const selectedDatabase = (databases ?? []).find( - (db) => db.identifier === state.selectedDatabaseId - ) - const isReplicaSelected = selectedDatabase?.identifier !== projectRef - - const { data: addons } = useProjectAddonsQuery({ projectRef }) - const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? []) - - const track = useTrack() - - const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at'] - const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' } - const connectionInfo = pluckObjectFields(selectedDatabase || emptyState, DB_FIELDS) - - const handleCopy = ( - connectionTypeId: string, - connectionStringMethod: 'direct' | 'transaction_pooler' | 'session_pooler' - ) => { - const connectionInfo = DATABASE_CONNECTION_TYPES.find((type) => type.id === connectionTypeId) - const connectionType = connectionInfo?.label ?? 'Unknown' - const lang = connectionInfo?.lang ?? 'Unknown' - track('connection_string_copied', { - connectionType, - lang, - connectionMethod: connectionStringMethod, - connectionTab: 'Connection String', - }) - } - - const supavisorConnectionStrings = getConnectionStrings({ - connectionInfo, - poolingInfo: { - connectionString: sharedPoolerConfig?.connection_string ?? '', - db_host: isReplicaSelected ? connectionInfo.db_host : (sharedPoolerConfig?.db_host ?? ''), - db_name: sharedPoolerConfig?.db_name ?? '', - db_port: sharedPoolerConfig?.db_port ?? 0, - db_user: sharedPoolerConfig?.db_user ?? '', - }, - metadata: { projectRef }, - }) - - const connectionStrings = getConnectionStrings({ - connectionInfo, - poolingInfo: { - connectionString: isReplicaSelected - ? (poolingConfiguration?.connection_string.replace( - poolingConfiguration?.db_host, - connectionInfo.db_host - ) ?? '') - : (poolingConfiguration?.connection_string ?? ''), - db_host: isReplicaSelected ? connectionInfo.db_host : poolingConfiguration?.db_host, - db_name: poolingConfiguration?.db_name ?? '', - db_port: poolingConfiguration?.db_port ?? 0, - db_user: poolingConfiguration?.db_user ?? '', - }, - metadata: { projectRef }, - }) - - const lang = DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.lang ?? 'bash' - const contentType = - DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.contentType ?? 'input' - - const example: Example | undefined = examples[selectedTab as keyof typeof examples] - - const exampleFiles = example?.files - const exampleInstallCommands = example?.installCommands - const examplePostInstallCommands = example?.postInstallCommands - const hasCodeExamples = exampleFiles || exampleInstallCommands - const fileTitle = DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.fileTitle - - // [Refactor] See if we can do this in an immutable way, technically not a good practice to do this - let stepNumber = 0 - - const ipv4AddOnUrl = { - text: 'IPv4 add-on', - url: `/project/${projectRef}/settings/addons?panel=ipv4`, - } - const ipv4SettingsUrl = { - text: 'IPv4 settings', - url: `/project/${projectRef}/settings/addons?panel=ipv4`, - } - const poolerSettingsUrl = { - text: 'Pooler settings', - url: `/project/${projectRef}/database/settings#connection-pooling`, - } - const buttonLinks = !ipv4Addon - ? [ipv4AddOnUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])] - : [ipv4SettingsUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])] - const poolerBadge = sharedPoolerPreferred ? 'Shared Pooler' : 'Dedicated Pooler' - - return ( -
-
-
-
- - Type - - -
- -
- - Method - - -
-
-

- Learn how to connect to your - Postgres databases. - - Read docs - -

-
- - {isLoading && ( -
- -
- )} - - {isError && ( -
- -
- )} - - {isSuccess && ( -
- {/* // handle non terminal examples */} - {hasCodeExamples && ( -
-
- - Install the following - - {exampleInstallCommands?.map((cmd) => ( - - {cmd} - - ))} -
- {exampleFiles && exampleFiles?.length > 0 && ( -
- - Add file to project - - {exampleFiles?.map((file) => ( -
- - -
- ))} -
- )} -
- )} - -
- {hasCodeExamples && ( -
- Connect to your database -
- )} -
- {selectedMethod === 'direct' && ( - handleCopy(selectedTab, 'direct')} - /> - )} - - {selectedMethod === 'transaction' && IS_PLATFORM && ( - handleCopy(selectedTab, 'transaction_pooler')} - > - {!sharedPoolerPreferred && !ipv4Addon && ( - <> - - - - - - - handleCopy(selectedTab, 'transaction_pooler')} - /> -

- Only recommended when your network does not support IPv6. Added latency - compared to dedicated pooler. -

-
-
- - )} -
- )} - - {selectedMethod === 'session' && IS_PLATFORM && ( - handleCopy(selectedTab, 'session_pooler')} - /> - )} -
-
- - {examplePostInstallCommands && ( -
- - Add the configuration package to read the settings - - {examplePostInstallCommands?.map((cmd) => ( - - {cmd} - - ))} -
- )} -
- )} - - {selectedTab === 'python' && ( - <> - - - -
-

- Connecting to SQL Alchemy -

- -
-
- -
-

- Please use postgresql:// instead of postgres:// as your - dialect when connecting via SQLAlchemy. -

-

- Example: - create_engine("postgresql+psycopg2://...") -

-

-
-
-
- - )} - - -
-

Reset your database password

-

- You may reset your database password in your project's{' '} - - Database Settings - -

-
-
- ) -} - -const ConnectionStringMethodSelectItem = ({ - method, - poolerBadge, -}: { - method: ConnectionStringMethod - poolerBadge?: string -}) => { - const badges: ReactNode[] = [] - - if (method !== 'direct') { - badges.push( - - Shared Pooler - - ) - } - if (poolerBadge === 'Dedicated Pooler') { - badges.push( - - {poolerBadge} - - ) - } - - return ( - -
-
- {connectionStringMethodOptions[method].label} -
-
- {connectionStringMethodOptions[method].description} -
-
- {badges.map((badge) => badge)} -
-
-
- ) -} diff --git a/apps/studio/components/interfaces/Connect/DirectConnectionExamples.tsx b/apps/studio/components/interfaces/Connect/DirectConnectionExamples.tsx deleted file mode 100644 index fb3b6c06c6c..00000000000 --- a/apps/studio/components/interfaces/Connect/DirectConnectionExamples.tsx +++ /dev/null @@ -1,151 +0,0 @@ -export type Example = { - installCommands?: string[] - postInstallCommands?: string[] - files?: { - name: string - content: string - }[] -} - -export const examples = { - nodejs: { - installCommands: ['npm install postgres'], - files: [ - { - name: 'db.js', - content: `import postgres from 'postgres' - -const connectionString = process.env.DATABASE_URL -const sql = postgres(connectionString) - -export default sql`, - }, - ], - }, - golang: { - installCommands: ['go get github.com/jackc/pgx/v5'], - files: [ - { - name: 'main.go', - content: `package main - -import ( - "context" - "log" - "os" - "github.com/jackc/pgx/v5" -) - -func main() { - conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL")) - if err != nil { - log.Fatalf("Failed to connect to the database: %v", err) - } - defer conn.Close(context.Background()) - - // Example query to test connection - var version string - if err := conn.QueryRow(context.Background(), "SELECT version()").Scan(&version); err != nil { - log.Fatalf("Query failed: %v", err) - } - - log.Println("Connected to:", version) -}`, - }, - ], - }, - dotnet: { - installCommands: [ - 'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION', - ], - postInstallCommands: [ - 'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION', - ], - }, - python: { - installCommands: ['pip install python-dotenv psycopg2'], - files: [ - { - name: 'main.py', - content: `import psycopg2 -from dotenv import load_dotenv -import os - -# Load environment variables from .env -load_dotenv() - -# Fetch variables -USER = os.getenv("user") -PASSWORD = os.getenv("password") -HOST = os.getenv("host") -PORT = os.getenv("port") -DBNAME = os.getenv("dbname") - -# Connect to the database -try: - connection = psycopg2.connect( - user=USER, - password=PASSWORD, - host=HOST, - port=PORT, - dbname=DBNAME - ) - print("Connection successful!") - - # Create a cursor to execute SQL queries - cursor = connection.cursor() - - # Example query - cursor.execute("SELECT NOW();") - result = cursor.fetchone() - print("Current Time:", result) - - # Close the cursor and connection - cursor.close() - connection.close() - print("Connection closed.") - -except Exception as e: - print(f"Failed to connect: {e}")`, - }, - ], - }, - sqlalchemy: { - installCommands: ['pip install python-dotenv sqlalchemy psycopg2'], - files: [ - { - name: 'main.py', - content: `from sqlalchemy import create_engine -# from sqlalchemy.pool import NullPool -from dotenv import load_dotenv -import os - -# Load environment variables from .env -load_dotenv() - -# Fetch variables -USER = os.getenv("user") -PASSWORD = os.getenv("password") -HOST = os.getenv("host") -PORT = os.getenv("port") -DBNAME = os.getenv("dbname") - -# Construct the SQLAlchemy connection string -DATABASE_URL = f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?sslmode=require" - -# Create the SQLAlchemy engine -engine = create_engine(DATABASE_URL) -# If using Transaction Pooler or Session Pooler, we want to ensure we disable SQLAlchemy client side pooling - -# https://docs.sqlalchemy.org/en/20/core/pooling.html#switching-pool-implementations -# engine = create_engine(DATABASE_URL, poolclass=NullPool) - -# Test the connection -try: - with engine.connect() as connection: - print("Connection successful!") -except Exception as e: - print(f"Failed to connect: {e}")`, - }, - ], - }, -} diff --git a/apps/studio/components/interfaces/Connect/McpTabContent.tsx b/apps/studio/components/interfaces/Connect/McpTabContent.tsx deleted file mode 100644 index 7f6bf6cd97d..00000000000 --- a/apps/studio/components/interfaces/Connect/McpTabContent.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { IS_PLATFORM, useParams } from 'common' -import { useTheme } from 'next-themes' -import { useMemo, useState } from 'react' -import { createMcpCopyHandler, McpConfigPanel, type McpClient } from 'ui-patterns/McpUrlBuilder' -import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' - -import type { projectKeys } from './Connect.types' -import Panel from '@/components/ui/Panel' -import { useTrack } from '@/lib/telemetry/track' - -export const McpTabContent = ({ projectKeys }: { projectKeys: projectKeys }) => { - const { ref: projectRef } = useParams() - - return ( - - {projectRef ? ( - - ) : ( - - )} - - ) -} - -const McpTabContentInnerLoading = () => { - return ( -
- - -
- ) -} - -const McpTabContentInnerLoaded = ({ - projectRef, - projectKeys, -}: { - projectRef: string - projectKeys: projectKeys -}) => { - const { resolvedTheme } = useTheme() - const track = useTrack() - const [selectedClient, setSelectedClient] = useState(null) - - const handleCopy = useMemo( - () => - createMcpCopyHandler({ - selectedClient, - source: 'studio', - onTrack: (event) => track(event.action, event.properties, event.groups), - projectRef, - }), - [selectedClient, track, projectRef] - ) - - const handleInstall = () => { - if (selectedClient?.label) { - track('mcp_install_button_clicked', { - client: selectedClient.label, - source: 'studio', - }) - } - } - - return ( - - ) -} diff --git a/apps/studio/components/interfaces/Connect/content/androidkotlin/supabasekt/content.tsx b/apps/studio/components/interfaces/Connect/content/androidkotlin/supabasekt/content.tsx deleted file mode 100644 index 66caa833bff..00000000000 --- a/apps/studio/components/interfaces/Connect/content/androidkotlin/supabasekt/content.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - {` -@Serializable -data class TodoItem(val id: Int, val name: String) - `} - - - - - - {` -val supabase = createSupabaseClient( - supabaseUrl = "${projectKeys.apiUrl ?? 'your-project-url'}", - supabaseKey = "${projectKeys.publishableKey ?? ''}" - ) { - install(Postgrest) -} - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - MaterialTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - TodoList() - } - } - } - } -} - -@Composable -fun TodoList() { - var items by remember { mutableStateOf>(listOf()) } - LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { - items = supabase.from("todos") - .select().decodeList() - } - } - LazyColumn { - items( - items, - key = { item -> item.id }, - ) { item -> - Text( - item.name, - modifier = Modifier.padding(8.dp), - ) - } - } -} -`} - - - - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/astro/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/astro/supabasejs/content.tsx deleted file mode 100644 index cf8c7c9dcc6..00000000000 --- a/apps/studio/components/interfaces/Connect/content/astro/supabasejs/content.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {` -SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} -SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'} - `} - - - - - - {` -import { createClient } from "@supabase/supabase-js"; - -const supabaseUrl = import.meta.env.SUPABASE_URL; -const supabaseKey = import.meta.env.SUPABASE_KEY; - -export const supabase = createClient(supabaseUrl, supabaseKey); - `} - - - - - - {` ---- -import { supabase } from '../db/supabase'; - -const { data, error } = await supabase.from("todos").select('*'); ---- - -{ - ( -
    - {data.map((entry) => ( -
  • {entry.name}
  • - ))} -
- ) -} -`} -
-
-
- ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/drizzle/content.tsx b/apps/studio/components/interfaces/Connect/content/drizzle/content.tsx deleted file mode 100644 index 2ef670083a6..00000000000 --- a/apps/studio/components/interfaces/Connect/content/drizzle/content.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ connectionStringPooler }: ContentFileProps) => { - return ( - - - - - - - - - - {connectionStringPooler.ipv4SupportedForDedicatedPooler && - connectionStringPooler.transactionDedicated - ? ` -DATABASE_URL="${connectionStringPooler.transactionDedicated}" - ` - : connectionStringPooler.transactionDedicated && - !connectionStringPooler.ipv4SupportedForDedicatedPooler - ? ` -# Use Shared connection pooler (supports both IPv4/IPv6) -DATABASE_URL="${connectionStringPooler.transactionShared}" - -# If your network supports IPv6 or you purchased IPv4 addon, use dedicated pooler -# DATABASE_URL="${connectionStringPooler.transactionDedicated}" - ` - : ` -DATABASE_URL="${connectionStringPooler.transactionShared}" -`} - - - - - - {` -import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core"; - -export const users = pgTable('users', { - id: serial('id').primaryKey(), - fullName: text('full_name'), - phone: varchar('phone', { length: 256 }), -}); - `} - - - - - - {` -import { drizzle } from 'drizzle-orm/postgres-js' -import postgres from 'postgres' -import { users } from './schema' - -const connectionString = process.env.DATABASE_URL - -// Disable prefetch as it is not supported for "Transaction" pool mode -const client = postgres(connectionString, { prepare: false }) -const db = drizzle(client); - -const allUsers = await db.select().from(users); - `} - - - - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/exporeactnative/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/exporeactnative/supabasejs/content.tsx deleted file mode 100644 index 04a5806333d..00000000000 --- a/apps/studio/components/interfaces/Connect/content/exporeactnative/supabasejs/content.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {` -EXPO_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} -EXPO_PUBLIC_SUPABASE_KEY=${projectKeys.publishableKey ?? ''} - `} - - - - - - {` -import 'react-native-url-polyfill/auto' -import AsyncStorage from '@react-native-async-storage/async-storage' -import { createClient, processLock } from '@supabase/supabase-js' - -export const supabase = createClient( - process.env.EXPO_PUBLIC_SUPABASE_URL!, - process.env.EXPO_PUBLIC_SUPABASE_KEY!, - { - auth: { - storage: AsyncStorage, - autoRefreshToken: true, - persistSession: true, - detectSessionInUrl: false, - lock: processLock, - }, - }) - `} - - - - - - {` -import React, { useState, useEffect } from 'react'; -import { View, Text, FlatList } from 'react-native'; -import { supabase } from '../utils/supabase'; - -export default function App() { - const [todos, setTodos] = useState([]); - - useEffect(() => { - const getTodos = async () => { - try { - const { data: todos, error } = await supabase.from('todos').select(); - - if (error) { - console.error('Error fetching todos:', error.message); - return; - } - - if (todos && todos.length > 0) { - setTodos(todos); - } - } catch (error) { - console.error('Error fetching todos:', error.message); - } - }; - - getTodos(); - }, []); - - return ( - - Todo List - item.id.toString()} - renderItem={({ item }) => {item.title}} - /> - - ); -}; - -`} - - - - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/flask/supabasepy/content.tsx b/apps/studio/components/interfaces/Connect/content/flask/supabasepy/content.tsx deleted file mode 100644 index 08ff15511e1..00000000000 --- a/apps/studio/components/interfaces/Connect/content/flask/supabasepy/content.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - {` -SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} -SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'} - `} - - - - - - {` -import os -from flask import Flask -from supabase import create_client, Client -from dotenv import load_dotenv - -load_dotenv() - -app = Flask(__name__) - -supabase: Client = create_client( - os.environ.get("SUPABASE_URL"), - os.environ.get("SUPABASE_KEY") -) - -@app.route('/') -def index(): - response = supabase.table('todos').select("*").execute() - todos = response.data - - html = '

Todos

    ' - for todo in todos: - html += f'
  • {todo["name"]}
  • ' - html += '
' - - return html - -if __name__ == '__main__': - app.run(debug=True) -`} -
-
-
- ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/flutter/supabaseflutter/content.tsx b/apps/studio/components/interfaces/Connect/content/flutter/supabaseflutter/content.tsx deleted file mode 100644 index 26356a92a19..00000000000 --- a/apps/studio/components/interfaces/Connect/content/flutter/supabaseflutter/content.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - {` -import 'package:supabase_flutter/supabase_flutter.dart'; - -Future main() async { - await Supabase.initialize( - url: '${projectKeys.apiUrl ?? 'your-project-url'}', - anonKey: '${projectKeys.publishableKey ?? ''}', - ); - runApp(MyApp()); -} - `} - - - - - - {` -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - title: 'Todos', - home: HomePage(), - ); - } -} - -class HomePage extends StatefulWidget { - const HomePage({super.key}); - - @override - State createState() => _HomePageState(); -} - -class _HomePageState extends State { - final _future = Supabase.instance.client - .from('todos') - .select(); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: FutureBuilder( - future: _future, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator()); - } - final todos = snapshot.data!; - return ListView.builder( - itemCount: todos.length, - itemBuilder: ((context, index) { - final todo = todos[index]; - return ListTile( - title: Text(todo['name']), - ); - }), - ); - }, - ), - ); - } -} -`} - - - - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx deleted file mode 100644 index 5470df4a99b..00000000000 --- a/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - - - {` -export const environment = { - supabaseUrl: '${projectKeys.apiUrl ?? 'your-project-url'}', - supabaseKey: '${projectKeys.publishableKey ?? ''}', -}; -`} - - - - - - {` -import { Injectable } from '@angular/core'; -import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { environment } from '../environments/environment'; - -@Injectable({ - providedIn: 'root', -}) -export class SupabaseService { - private supabase: SupabaseClient; - constructor() { - this.supabase = createClient( - environment.supabaseUrl, - environment.supabaseKey - ); - } - - getTodos() { - return this.supabase.from('todos').select('*'); - } -} -`} - - - - - - {` -import { Component, OnInit } from '@angular/core'; -import { SupabaseService } from './supabase.service'; - -@Component({ - selector: 'app-root', - templateUrl: 'app.component.html', - styleUrls: ['app.component.scss'], -}) -export class AppComponent implements OnInit { - todos: any[] = []; - - constructor(private supabaseService: SupabaseService) {} - - async ngOnInit() { - await this.loadTodos(); - } - - async loadTodos() { - const { data, error } = await this.supabaseService.getTodos(); - if (error) { - console.error('Error fetching todos:', error); - } else { - this.todos = data; - } - } -} -`} - - - - - - {` - - - Todo List - - - - - - - {{ todo.title }} - - - -`} - - - - - - {` -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { BrowserModule } from '@angular/platform-browser'; -import { RouterModule } from '@angular/router'; - -import { IonicModule } from '@ionic/angular'; - -import { AppComponent } from './app.component'; -import { SupabaseService } from './supabase.service'; - -@NgModule({ - imports: [ - BrowserModule, - FormsModule, - RouterModule.forRoot([]), - IonicModule.forRoot({ mode: 'ios' }), - ], - declarations: [AppComponent], - providers: [SupabaseService], - bootstrap: [AppComponent], -}) -export class AppModule {} -`} - - - - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/ionicreact/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/ionicreact/supabasejs/content.tsx deleted file mode 100644 index 2121f100ac6..00000000000 --- a/apps/studio/components/interfaces/Connect/content/ionicreact/supabasejs/content.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {` -REACT_APP_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} -REACT_APP_SUPABASE_KEY=${projectKeys.publishableKey ?? ''} - `} - - - - - - {` -import { createClient } from '@supabase/supabase-js' - -const supabaseUrl = process.env.REACT_APP_SUPABASE_URL -const supabaseKey = process.env.REACT_APP_SUPABASE_KEY - -export const supabase = createClient(supabaseUrl, supabaseAnonKey) -`} - - - - - - {` -import React, { useEffect, useState } from 'react'; -import { setupIonicReact, IonApp } from '@ionic/react'; -import { - IonContent, - IonHeader, - IonTitle, - IonToolbar, - IonList, - IonItem, -} from '@ionic/react'; - -/* Core CSS required for Ionic components to work properly */ -import '@ionic/react/css/core.css'; - -/* Theme variables */ -import './theme/variables.css'; - -import { supabase } from './supabaseClient'; - -setupIonicReact(); - -export default function App() { - const [todos, setTodos] = useState([]); - useEffect(() => { - getTodos(); - }, []); - - const getTodos = async () => { - try { - const { data, error } = await supabase.from('todos').select(); - - if (error) { - console.error('Error fetching todos:', error.message); - return; - } - - if (data) { - setTodos(data); - } - } catch (error) { - console.error('Error fetching todos:', error.message); - } - }; - - return ( - - <> - - - Todos - - - - - {todos.map((todo) => ( - {todo.title} - ))} - - - - - ); -} -`} - - - - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/nextjs/app/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/nextjs/app/supabasejs/content.tsx deleted file mode 100644 index 5a9bb68f2d4..00000000000 --- a/apps/studio/components/interfaces/Connect/content/nextjs/app/supabasejs/content.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - - - {[ - '', - `NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, - projectKeys?.publishableKey - ? `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=${projectKeys.publishableKey}` - : `NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, - '', - ].join('\n')} - - - - - - {` -import { createClient } from '@/utils/supabase/server' -import { cookies } from 'next/headers' - -export default async function Page() { - const cookieStore = await cookies() - const supabase = createClient(cookieStore) - - const { data: todos } = await supabase.from('todos').select() - - return ( -
    - {todos?.map((todo) => ( -
  • {todo}
  • - ))} -
- ) -} -`} -
-
- - - - {` -import { createServerClient, type CookieOptions } from "@supabase/ssr"; -import { cookies } from "next/headers"; - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; -const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'}; - -export const createClient = (cookieStore: ReturnType) => { - return createServerClient( - supabaseUrl!, - supabaseKey!, - { - 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. - } - }, - }, - }, - ); -}; -`} - - - - - {` -import { createBrowserClient } from "@supabase/ssr"; - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; -const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'}; - -export const createClient = () => - createBrowserClient( - supabaseUrl!, - supabaseKey!, - ); -`} - - - - - - {` -import { createServerClient, type CookieOptions } from "@supabase/ssr"; -import { type NextRequest, NextResponse } from "next/server"; - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; -const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'}; - -export const createClient = (request: NextRequest) => { - // Create an unmodified response - let supabaseResponse = NextResponse.next({ - request: { - headers: request.headers, - }, - }); - - const supabase = createServerClient( - supabaseUrl!, - supabaseKey!, - { - 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) - ) - }, - }, - }, - ); - - return supabaseResponse -}; -`} - - -
- ) -} - -// [Joshen] Used as a dynamic import -// eslint-disable-next-line no-restricted-exports -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/nextjs/pages/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/nextjs/pages/supabasejs/content.tsx deleted file mode 100644 index 2fdddb466aa..00000000000 --- a/apps/studio/components/interfaces/Connect/content/nextjs/pages/supabasejs/content.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {[ - '', - `NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, - projectKeys?.publishableKey - ? `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=${projectKeys.publishableKey}` - : `NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, - '', - ].join('\n')} - - - - - - {` -import { createClient } from "@supabase/supabase-js"; - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; -const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'}; - -export const supabase = createClient(supabaseUrl, supabaseKey); - `} - - - - - - {` -import { useState, useEffect } from 'react' -import { supabase } from '../utils/supabase' - -function Page() { - const [todos, setTodos] = useState([]) - - useEffect(() => { - async function getTodos() { - const { data: todos } = await supabase.from('todos').select() - - if (todos.length > 1) { - setTodos(todos) - } - } - - getTodos() - }, []) - - return ( -
- {todos.map((todo) => ( -
  • {todo}
  • - ))} -
    - ) -} -export default Page - -`} -
    -
    -
    - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/nuxt/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/nuxt/supabasejs/content.tsx deleted file mode 100644 index 1c9cfe2633a..00000000000 --- a/apps/studio/components/interfaces/Connect/content/nuxt/supabasejs/content.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {` -SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} -SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'} - `} - - - - - - {` -import { createClient } from "@supabase/supabase-js"; - -const supabaseUrl = process.env.SUPABASE_URL; -const supabaseKey = process.env.SUPABASE_KEY; - -export const supabase = createClient(supabaseUrl, supabaseKey); - `} - - - - - - {` - - - -`} - - - - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/prisma/content.tsx b/apps/studio/components/interfaces/Connect/content/prisma/content.tsx deleted file mode 100644 index 89b1552a607..00000000000 --- a/apps/studio/components/interfaces/Connect/content/prisma/content.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' -import { IS_PLATFORM } from '@/lib/constants' - -const ContentFile = ({ connectionStringPooler }: ContentFileProps) => { - return ( - - - - - - - - - {connectionStringPooler.ipv4SupportedForDedicatedPooler && - connectionStringPooler.transactionDedicated - ? ` -# Connect to Supabase via connection pooling. -DATABASE_URL="${connectionStringPooler.transactionDedicated}?pgbouncer=true" - -# Direct connection to the database. Used for migrations. -DIRECT_URL="${connectionStringPooler.sessionDedicated}" - ` - : connectionStringPooler.transactionDedicated && - !connectionStringPooler.ipv4SupportedForDedicatedPooler - ? ` -# Connect to Supabase via Shared Connection Pooler -DATABASE_URL="${connectionStringPooler.transactionShared}?pgbouncer=true" - -# Direct connection to the database through Shared Pooler (supports IPv4/IPv6). Used for migrations. -DIRECT_URL="${connectionStringPooler.sessionShared}" - -# If your network supports IPv6 or you purchased IPv4 addon, use dedicated pooler -# DATABASE_URL="${connectionStringPooler.transactionDedicated}?pgbouncer=true" -# DIRECT_URL="${connectionStringPooler.sessionDedicated}" - ` - : ` -# Connect to Supabase ${IS_PLATFORM ? 'via connection pooling' : ''} -DATABASE_URL="${IS_PLATFORM ? `${connectionStringPooler.transactionShared}?pgbouncer=true` : connectionStringPooler.direct}" - -# Direct connection to the database. Used for migrations -DIRECT_URL="${IS_PLATFORM ? connectionStringPooler.sessionShared : connectionStringPooler.direct}" -`} - - - - - - {` -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - directUrl = env("DIRECT_URL") -} - `} - - - - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/react/create-react-app/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/react/create-react-app/supabasejs/content.tsx deleted file mode 100644 index c2c865eaadc..00000000000 --- a/apps/studio/components/interfaces/Connect/content/react/create-react-app/supabasejs/content.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {[ - '', - `REACT_APP_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, - projectKeys?.publishableKey - ? `REACT_APP_SUPABASE_PUBLISHABLE_KEY=${projectKeys.publishableKey}` - : `REACT_APP_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, - '', - ].join('\n')} - - - - - - {` -import { createClient } from "@supabase/supabase-js"; - -const supabaseUrl = process.env.REACT_APP_SUPABASE_URL; -const supabaseKey = process.env.${projectKeys.publishableKey ? 'REACT_APP_SUPABASE_PUBLISHABLE_KEY' : 'REACT_APP_SUPABASE_ANON_KEY'}; - -export const supabase = createClient(supabaseUrl, supabaseKey); - `} - - - - - - {` -import { useState, useEffect } from 'react' -import { supabase } from '../utils/supabase' - -function Page() { - const [todos, setTodos] = useState([]) - - useEffect(() => { - function getTodos() { - const { data: todos } = await supabase.from('todos').select() - - if (todos.length > 1) { - setTodos(todos) - } - } - - getTodos() - }, []) - - return ( -
    - {todos.map((todo) => ( -
  • {todo}
  • - ))} -
    - ) -} -export default Page -`} -
    -
    -
    - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/react/vite/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/react/vite/supabasejs/content.tsx deleted file mode 100644 index df57ef0ab37..00000000000 --- a/apps/studio/components/interfaces/Connect/content/react/vite/supabasejs/content.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {[ - '', - `VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, - projectKeys?.publishableKey - ? `VITE_SUPABASE_PUBLISHABLE_KEY=${projectKeys.publishableKey}` - : `VITE_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, - '', - ].join('\n')} - - - - - - {` -import { createClient } from '@supabase/supabase-js'; - -const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; -const supabaseKey = import.meta.env.${projectKeys.publishableKey ? 'VITE_SUPABASE_PUBLISHABLE_KEY' : 'VITE_SUPABASE_ANON_KEY'}; - -const supabase = createClient(supabaseUrl, supabaseKey); - -export default supabase - `} - - - - - - {` -import { useState, useEffect } from 'react' -import { supabase } from '../utils/supabase' - -function Page() { - const [todos, setTodos] = useState([]) - - useEffect(() => { - function getTodos() { - const { data: todos } = await supabase.from('todos').select() - - if (todos.length > 1) { - setTodos(todos) - } - } - - getTodos() - }, []) - - return ( -
    - {todos.map((todo) => ( -
  • {todo}
  • - ))} -
    - ) -} -export default Page -`} -
    -
    -
    - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/refine/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/refine/supabasejs/content.tsx deleted file mode 100644 index 36493112614..00000000000 --- a/apps/studio/components/interfaces/Connect/content/refine/supabasejs/content.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {[ - '', - `SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, - `SUPABASE_KEY=${projectKeys?.publishableKey ?? projectKeys?.anonKey ?? 'your-anon-key'}`, - '', - ].join('\n')} - - - - - - {` -import { createClient } from "@refinedev/supabase"; - -const SUPABASE_URL = process.env.SUPABASE_URL; -const SUPABASE_KEY = process.env.SUPABASE_KEY; - -export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY, { - db: { - schema: "public", - }, - auth: { - persistSession: true, - }, -}); - `} - - - - - - {` -import { Refine } from "@refinedev/core"; -import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar"; -import routerProvider, { - DocumentTitleHandler, - NavigateToResource, - UnsavedChangesNotifier, -} from "@refinedev/react-router"; -import { dataProvider, liveProvider } from "@refinedev/supabase"; -import { BrowserRouter, Route, Routes } from "react-router-dom"; - -import "./App.css"; -import authProvider from "./authProvider"; -import { supabaseClient } from "./utility"; -import { CountriesCreate, CountriesEdit, CountriesList, CountriesShow } from "./pages/countries"; - -function App() { - return ( - - - - - } - /> - - } /> - } /> - } /> - } /> - - - - - - - - - ); -} - -export default App; -`} - - - - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/remix/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/remix/supabasejs/content.tsx deleted file mode 100644 index 8f65ce355d2..00000000000 --- a/apps/studio/components/interfaces/Connect/content/remix/supabasejs/content.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {[ - '', - `VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, - projectKeys?.publishableKey - ? `VITE_SUPABASE_PUBLISHABLE_KEY=${projectKeys.publishableKey}` - : `VITE_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, - '', - ].join('\n')} - - - - - - {` -import { - createServerClient, - parseCookieHeader, - serializeCookieHeader, -} from "@supabase/ssr"; - -export function createClient(request: Request) { - const headers = new Headers(); - - const supabase = createServerClient( - process.env.VITE_SUPABASE_URL!, - process.env.VITE_${projectKeys.publishableKey ? 'SUPABASE_PUBLISHABLE_KEY' : 'SUPABASE_ANON_KEY'}!, - { - cookies: { - getAll() { - return parseCookieHeader(request.headers.get("Cookie") ?? "") as { - name: string; - value: string; - }[]; - }, - setAll(cookiesToSet) { - cookiesToSet.forEach(({ name, value, options }) => - headers.append( - "Set-Cookie", - serializeCookieHeader(name, value, options) - ) - ); - }, - }, - } - ); - - return { supabase, headers }; -} -`} - - - - - - {` -import type { Route } from "./+types/home"; -import { createClient } from "~/utils/supabase.server"; - -export async function loader({ request }: Route.LoaderArgs) { - const { supabase } = createClient(request); - const { data: todos } = await supabase.from("todos").select(); - - return { todos }; -} - -export default function Home({ loaderData }: Route.ComponentProps) { - return ( - <> -
      - {loaderData.todos?.map((todo) => ( -
    • {todo.name}
    • - ))} -
    - - ); -} - -`} -
    -
    -
    - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/solidjs/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/solidjs/supabasejs/content.tsx deleted file mode 100644 index 57dea50966a..00000000000 --- a/apps/studio/components/interfaces/Connect/content/solidjs/supabasejs/content.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {[ - '', - `SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, - projectKeys?.publishableKey - ? `SUPABASE_PUBLISHABLE_KEY=${projectKeys.publishableKey}` - : `SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, - '', - ].join('\n')} - - - - - - {` -import { createClient } from "@supabase/supabase-js"; - -const supabaseUrl = process.env.SUPABASE_URL; -const supabaseKey = process.env.${projectKeys.publishableKey ? 'SUPABASE_PUBLISHABLE_KEY' : 'SUPABASE_ANON_KEY'}; - -export const supabase = createClient(supabaseUrl!, supabaseKey!); - `} - - - - - - {` -import { supabase } from '../utils/supabase' -import { createResource, For } from "solid-js"; - -async function getTodos() { - const { data: todos } = await supabase.from("todos").select(); - return data; -} - -function App() { - const [todos] = createResource(getTodos); - - return ( -
      - {(country) =>
    • {todo.name}
    • }
      -
    - ); -} - -export default App; -`} -
    -
    -
    - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/sveltekit/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/sveltekit/supabasejs/content.tsx deleted file mode 100644 index ab13fb0546b..00000000000 --- a/apps/studio/components/interfaces/Connect/content/sveltekit/supabasejs/content.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - - {[ - '', - `PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`, - projectKeys?.publishableKey - ? `PUBLIC_SUPABASE_PUBLISHABLE_KEY=${projectKeys.publishableKey}` - : `PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`, - '', - ].join('\n')} - - - - - - {` -import { createClient } from "@supabase/supabase-js"; -import { PUBLIC_SUPABASE_URL, ${projectKeys.publishableKey ? 'PUBLIC_SUPABASE_PUBLISHABLE_KEY' : 'PUBLIC_SUPABASE_ANON_KEY'} } from "$env/static/public" - -const supabaseUrl = PUBLIC_SUPABASE_URL; -const supabaseKey = ${projectKeys.publishableKey ? 'PUBLIC_SUPABASE_PUBLISHABLE_KEY' : 'PUBLIC_SUPABASE_ANON_KEY'}; - -export const supabase = createClient(supabaseUrl, supabaseKey); - `} - - - - - - {` -import { supabase } from "$lib/supabaseClient"; - -export async function load() { - const { data } = await supabase.from("countries").select(); - return { - countries: data ?? [], - }; -} -`} - - - - - - {` - - -
      - {#each data.countries as country} -
    • {country.name}
    • - {/each} -
    -`} -
    -
    -
    - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/swift/supabaseswift/content.tsx b/apps/studio/components/interfaces/Connect/content/swift/supabaseswift/content.tsx deleted file mode 100644 index 2fd5bf18cf4..00000000000 --- a/apps/studio/components/interfaces/Connect/content/swift/supabaseswift/content.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {` -import Foundation -import Supabase - -let supabase = SupabaseClient( - supabaseURL: URL(string: "${projectKeys.apiUrl ?? 'your-project-url'}")!, - supabaseKey: "${projectKeys.publishableKey ?? ''}" -) - `} - - - - - - {` -import Foundation - -struct Todo: Identifiable, Decodable { - var id: Int - var title: String -} -`} - - - - - - {` -import Supabase -import SwiftUI - -struct ContentView: View { - @State var todos: [Todo] = [] - - var body: some View { - NavigationStack { - List(todos) { todo in - Text(todo.title) - } - .navigationTitle("Todos") - .task { - do { - todos = try await supabase.from("todos").select().execute().value - } catch { - debugPrint(error) - } - } - } - } -} - -#Preview { - ContentView() -} - -`} - - - - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/tanstack/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/tanstack/supabasejs/content.tsx deleted file mode 100644 index 2c36da5d106..00000000000 --- a/apps/studio/components/interfaces/Connect/content/tanstack/supabasejs/content.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {` -VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} -VITE_SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'} - `} - - - - - - {` -import { createClient } from "@supabase/supabase-js"; - -export const supabase = createClient( - import.meta.env.VITE_SUPABASE_URL, - import.meta.env.VITE_SUPABASE_KEY -); - `} - - - - - - {` -import { createFileRoute } from '@tanstack/react-router' -import { supabase } from '../utils/supabase' - -export const Route = createFileRoute('/')({ - loader: async () => { - const { data: todos } = await supabase.from('todos').select() - return { todos } - }, - component: Home, -}) - -function Home() { - const { todos } = Route.useLoaderData() - - return ( -
      - {todos?.map((todo) => ( -
    • {todo.name}
    • - ))} -
    - ) -} -`} -
    -
    -
    - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/Connect/content/vuejs/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/vuejs/supabasejs/content.tsx deleted file mode 100644 index 70e70bc39a5..00000000000 --- a/apps/studio/components/interfaces/Connect/content/vuejs/supabasejs/content.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' - -import type { ContentFileProps } from '@/components/interfaces/Connect/Connect.types' -import { - ConnectTabContent, - ConnectTabs, - ConnectTabTrigger, - ConnectTabTriggers, -} from '@/components/interfaces/Connect/ConnectTabs' - -const ContentFile = ({ projectKeys }: ContentFileProps) => { - return ( - - - - - - - - - - {` -SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'} -SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'} - `} - - - - - - {` -import { createClient } from "@supabase/supabase-js"; - -const supabaseUrl = process.env.SUPABASE_URL; -const supabaseKey = process.env.SUPABASE_KEY; - -export const supabase = createClient(supabaseUrl, supabaseKey); - `} - - - - - - {` - - - -`} - - - - ) -} - -export default ContentFile diff --git a/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts b/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts index 63beba7ac22..675d6d7c473 100644 --- a/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts +++ b/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts @@ -57,7 +57,7 @@ export type ConditionalValue = // Schema Types - Modes // ============================================================================ -export const CONNECT_MODES = ['framework', 'direct', 'orm', 'mcp'] as const +export const CONNECT_MODES = ['framework', 'direct', 'orm', 'mcp', 'server'] as const export type ConnectMode = (typeof CONNECT_MODES)[number] export interface ModeDefinition { @@ -65,6 +65,7 @@ export interface ModeDefinition { label: string description: string icon?: string + prompt?: string fields: string[] // References to field IDs } diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx index b7b48b17bf6..566bc55d720 100644 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx @@ -1,4 +1,4 @@ -import { Box, Cable, Database, Sparkles } from 'lucide-react' +import { Box, Cable, Database, Server, Sparkles } from 'lucide-react' import { cn, RadioGroupStacked, @@ -27,6 +27,7 @@ const MODE_ICONS: Record = { direct: , orm: , mcp: , + server: , } interface ConnectConfigSectionProps { @@ -244,7 +245,10 @@ interface ModeSelectorProps { export function ModeSelector({ modes, selected, onChange }: ModeSelectorProps) { return ( -
    +
    {modes.map((mode) => (
    -
    - -
    + {activeFields.length > 0 && ( +
    + +
    + )}
    diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx index 5b8898ef283..b9825fa2360 100644 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx @@ -6,6 +6,7 @@ import { Button } from 'ui' import { Admonition } from 'ui-patterns' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' +import { connectSchema } from './connect.schema' import type { ConnectionStringPooler, ConnectState, @@ -195,6 +196,11 @@ export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsS const showSelfHostedMcpNotice = deploymentMode.isSelfHosted && state.mode === 'mcp' + const customPrompt = useMemo( + () => connectSchema.modes.find((m) => m.id === state.mode)?.prompt, + [state.mode] + ) + if (steps.length === 0) return null return ( @@ -238,7 +244,7 @@ export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsS /> )} - +
    {steps.map((step, index) => ( diff --git a/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx b/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx index ea9f67d8155..6b60b9065f3 100644 --- a/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx +++ b/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx @@ -6,6 +6,8 @@ import { BASE_PATH } from '@/lib/constants' interface CopyPromptAdmonitionProps { stepsContainerRef: RefObject + /** When set, the Copy prompt button uses this verbatim instead of scraping the steps. */ + customPrompt?: string } const normalizeTextLines = (value: string) => { @@ -123,9 +125,12 @@ export const buildConnectPrompt = (stepsContainer: HTMLElement | null) => { return promptContent } -export function CopyPromptAdmonition({ stepsContainerRef }: CopyPromptAdmonitionProps) { +export function CopyPromptAdmonition({ + stepsContainerRef, + customPrompt, +}: CopyPromptAdmonitionProps) { const handleCopyPrompt = () => { - return buildConnectPrompt(stepsContainerRef.current) + return customPrompt ?? buildConnectPrompt(stepsContainerRef.current) } return ( diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts index 5db2b1addfc..15204084a0f 100644 --- a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts +++ b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts @@ -166,6 +166,22 @@ const ormConfigureStep: StepDefinition = { content: '{{orm}}', } +const serverInstallStep: StepDefinition = { + id: 'server-install', + title: 'Install package', + description: + 'Add @supabase/server to your backend or API framework of choice. On Supabase Edge Functions you can import it directly, no install needed.', + content: 'server/install', +} + +const serverEnvStep: StepDefinition = { + id: 'server-env', + title: 'Set environment variables', + description: + 'Copy these into your environment so you can verify users and use the client/admin supabase-js library from the context of your handler. On Supabase Edge Functions they are injected automatically.', + content: 'server/env', +} + const skillsInstallStep: StepDefinition = { id: 'install-skills', title: 'Install Agent Skills (Optional)', @@ -174,6 +190,45 @@ const skillsInstallStep: StepDefinition = { content: 'steps/skills-install', } +const serverSkillsInstallStep: StepDefinition = { + id: 'install-skills', + title: 'Install the Supabase Server skill (Optional)', + description: + 'Gives AI coding tools ready-made instructions for building APIs with @supabase/server.', + content: 'steps/skills-install', +} + +// ============================================================================ +// Mode Prompts +// ============================================================================ + +// Agent-ready prompt for the Server mode. Intentionally omits the project's +// actual keys — the secret should never be pasted into an LLM prompt; users +// copy the real values from the env step. +const serverConnectPrompt = `Set up the @supabase/server SDK in this project. + +Install it: +npm install @supabase/server + +It reads these environment variables (copy the real values from the Supabase dashboard's Connect dialog — never commit the secret key): +- SUPABASE_URL +- SUPABASE_PUBLISHABLE_KEY +- SUPABASE_SECRET_KEY +- SUPABASE_JWKS_URL (used to verify user JWTs) + +Create request handlers with \`withSupabase\` from "@supabase/server". It validates auth and provides an RLS-scoped client (\`ctx.supabase\`) and an admin client that bypasses RLS (\`ctx.supabaseAdmin\`). Example: + +import { withSupabase } from "@supabase/server" + +export default { + fetch: withSupabase({ auth: "user" }, async (_req, ctx) => { + const { data } = await ctx.supabase.from("todos").select() + return Response.json(data) + }), +} + +Auth modes: "user" (valid JWT), "publishable" (publishable key), "secret" (secret key), "none". On Supabase Edge Functions these env vars are injected automatically; for non-"user" auth modes, set \`verify_jwt = false\` for the function in supabase/config.toml.` + // ============================================================================ // Main Schema // ============================================================================ @@ -189,6 +244,13 @@ export const connectSchema: ConnectSchema = { description: 'Use a client library', fields: ['framework', 'frameworkVariant', 'library', 'frameworkUi'], }, + { + id: 'server', + label: 'Server', + description: 'Build APIs', + fields: [], + prompt: serverConnectPrompt, + }, { id: 'direct', label: 'Direct', @@ -386,6 +448,7 @@ export const connectSchema: ConnectSchema = { DEFAULT: [mcpConfigureStep, skillsInstallStep], }, }, + server: [serverInstallStep, serverEnvStep, serverSkillsInstallStep], DEFAULT: [skillsInstallStep], }, }, diff --git a/apps/studio/components/interfaces/ConnectSheet/content/server/common/EnvRow.tsx b/apps/studio/components/interfaces/ConnectSheet/content/server/common/EnvRow.tsx new file mode 100644 index 00000000000..47a26028642 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/server/common/EnvRow.tsx @@ -0,0 +1,19 @@ +export function EnvRow({ + name, + value, + children, +}: { + name: string + value: string + children: React.ReactNode +}) { + return ( +
    + {name}= + + {value} + +
    {children}
    +
    + ) +} diff --git a/apps/studio/components/interfaces/ConnectSheet/content/server/common/SecretRow.tsx b/apps/studio/components/interfaces/ConnectSheet/content/server/common/SecretRow.tsx new file mode 100644 index 00000000000..62101a6f9a7 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/server/common/SecretRow.tsx @@ -0,0 +1,64 @@ +import { Eye, EyeOff } from 'lucide-react' +import { toast } from 'sonner' +import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' + +import { ConnectServerEnvSecret, SERVER_ENV_VARS } from '../../../useConnectServerEnv' +import { EnvRow } from './EnvRow' +import CopyButton from '@/components/ui/CopyButton' + +export interface SecretEnvRowProps { + secret: ConnectServerEnvSecret +} + +export function SecretEnvRow({ secret }: SecretEnvRowProps) { + const isDisabled = !secret.exists || !secret.canReveal + + const onToggle = () => { + secret.toggle().catch(() => toast.error('Failed to reveal secret API key')) + } + + const onCopy = async () => { + try { + return await secret.getValue() + } catch { + toast.error('Failed to copy secret API key') + return '' + } + } + + const revealTooltip = !secret.exists + ? 'No secret key found for this project' + : !secret.canReveal + ? 'You need additional permissions to reveal secret API keys' + : secret.isRevealed + ? 'Hide secret key' + : 'Reveal secret key' + + return ( + + + + , + ] + : undefined + } + /> +
    + ) +} + +export default ServerEnvContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/server/install/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/server/install/content.tsx new file mode 100644 index 00000000000..0d768dd1741 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/content/server/install/content.tsx @@ -0,0 +1,50 @@ +import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui' + +import CopyButton from '@/components/ui/CopyButton' + +const INSTALL_OPTIONS = [ + { name: 'npm', command: 'npm install @supabase/server' }, + { name: 'pnpm', command: 'pnpm add @supabase/server' }, + { name: 'bun', command: 'bun add @supabase/server' }, + { name: 'Deno', command: 'import { withSupabase } from "npm:@supabase/server"' }, +] + +function ServerInstallContent() { + return ( + + + {INSTALL_OPTIONS.map((option) => ( + + {option.name} + + ))} + + {INSTALL_OPTIONS.map((option) => ( + +
    + + {option.command} + + +
    +
    + ))} +
    + ) +} + +export default ServerInstallContent diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/skills-install/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/skills-install/content.tsx index 8ce58b7320a..0943a9da403 100644 --- a/apps/studio/components/interfaces/ConnectSheet/content/steps/skills-install/content.tsx +++ b/apps/studio/components/interfaces/ConnectSheet/content/steps/skills-install/content.tsx @@ -4,13 +4,16 @@ import { Button, copyToClipboard } from 'ui' import type { StepContentProps } from '@/components/interfaces/ConnectSheet/Connect.types' -const SKILLS_COMMAND = 'npx skills add supabase/agent-skills' +const DEFAULT_SKILLS_COMMAND = 'npx skills add supabase/agent-skills' +const SERVER_SKILLS_COMMAND = 'npx skills add supabase/server' -function SkillsInstallContent(_props: StepContentProps) { +function SkillsInstallContent({ state }: StepContentProps) { const [copyLabel, setCopyLabel] = useState('Copy') + const skillsCommand = state.mode === 'server' ? SERVER_SKILLS_COMMAND : DEFAULT_SKILLS_COMMAND + const handleCopy = () => { - copyToClipboard(SKILLS_COMMAND, () => { + copyToClipboard(skillsCommand, () => { setCopyLabel('Copied') setTimeout(() => setCopyLabel('Copy'), 2000) }) @@ -19,7 +22,7 @@ function SkillsInstallContent(_props: StepContentProps) { return (
    - {SKILLS_COMMAND} + {skillsCommand}
    + ) : isErrorBuckets ? ( + + ) : ( + + + + )} + + )} + /> + + ( + + {isLoadingNamespaces && canSelectNamespace ? ( + + ) : isErrorNamespaces ? ( + + ) : ( + + + + )} + + )} + /> + + {namespace === CREATE_NEW_NAMESPACE && ( + ( + + + + + + )} + /> + )} + + ( + + Automatically retrieved from your project's{' '} + + service role key + + + } + > + +
    + ) : null + } + /> + + )} + /> + + ( + +

    + Access keys are managed in your Storage{' '} + S3 settings +

    + + {isSuccessKeys && keyNoLongerExists && ( + + Please select another key or create a new set, as this destination will + not work otherwise. S3 access keys can be managed in your{' '} + + storage settings + + . + + } + /> + )} + + {s3AccessKeyId === CREATE_NEW_KEY && ( + + )} +
    + } + > + {isLoadingKeys ? ( + + ) : isErrorKeys ? ( + + ) : ( + + + + )} + + )} + /> + + {s3AccessKeyId !== CREATE_NEW_KEY && ( + ( + + + + +
    FiltersRLSConnected clientsTotal DB changes /secMax messages per client /secMax total messages /secLatency p95 + {heading} +