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({}) {
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"
>
-
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:
-
+```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" />
+
-## 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"
>
-
-
@@ -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:
-
+```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

-
-
### 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:

-#### 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"
>
-
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: .'
- expect(prefixInternalLinks(input, BASE)).toBe(input)
- })
-
- it('rewrites a link adjacent to an image without touching the image', () => {
- expect(prefixInternalLinks(' and [home](/dashboard)', BASE)).toBe(
- ' and [home](https://supabase.com/dashboard)'
- )
+ it('does not rewrite image URLs', () => {
+ expect(linkUrls('')).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 (``), 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 @@
-
\ 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 @@
-
-
-
\ 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 @@
-
-
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 @@
-
\ 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 @@
-
\ 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 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 ? (
- 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.
-
- )
-}
diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx
deleted file mode 100644
index 1dd6a049805..00000000000
--- a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx
+++ /dev/null
@@ -1,236 +0,0 @@
-import { useParams } from 'common'
-import { Auth, EdgeFunctions, Realtime, SqlEditor, Storage, TableEditor } from 'icons'
-import { ExternalLink, Settings } from 'lucide-react'
-import Link from 'next/link'
-import { Button } from 'ui'
-
-import { APIKeys } from './APIKeys'
-import { GetStartedHero } from './GetStartedHero'
-import { DocsButton } from '@/components/ui/DocsButton'
-import { InlineLink } from '@/components/ui/InlineLink'
-import Panel from '@/components/ui/Panel'
-import { EditorIndexPageLink } from '@/data/prefetchers/project.$ref.editor'
-import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
-import { DOCS_URL } from '@/lib/constants'
-
-export const NewProjectPanel = () => {
- const { ref } = useParams()
-
- const {
- projectAuthAll: authEnabled,
- projectEdgeFunctionAll: edgeFunctionsEnabled,
- projectStorageAll: storageEnabled,
- } = useIsFeatureEnabled(['project_auth:all', 'project_edge_function:all', 'project_storage:all'])
-
- return (
-
-
-
-
-
-
Welcome to your new project
-
- Your project has been deployed on its own instance, with its own API all set up and
- ready to use.
-
-
-
-
-
-
-
-
Build out your database
-
- Start building your app by creating tables and inserting data. Our Table Editor
- makes Postgres as easy to use as a spreadsheet, but there's also our SQL Editor if
- you need something more.
-
- )
-})
+ )
+ }
+)
interface IntegrationConnectionHeader extends React.HTMLAttributes {
name?: string
diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StatusDisplay.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StatusDisplay.tsx
index 488845d21c8..85bb055f2b5 100644
--- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StatusDisplay.tsx
+++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StatusDisplay.tsx
@@ -1,5 +1,5 @@
+import { SchemaInstallationStatus } from '@stripe/sync-engine/supabase'
import { AlertCircle, Check, RefreshCwIcon } from 'lucide-react'
-import { SchemaInstallationStatus } from 'stripe-experiment-sync/supabase'
import {
hasInstallError,
diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx
index a0bb8094a39..4e15dbd1740 100644
--- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx
+++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx
@@ -1,6 +1,6 @@
+import { SchemaInstallationStatus } from '@stripe/sync-engine/supabase'
import { EdgeFunctions } from 'icons'
import { Layers, Loader2, Table } from 'lucide-react'
-import { SchemaInstallationStatus } from 'stripe-experiment-sync/supabase'
import { Card, CardContent, cn } from 'ui'
import {
diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts
index dc5c8c3eb1f..2435e67f208 100644
--- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts
+++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts
@@ -1,4 +1,4 @@
-import { SchemaInstallationStatus, StripeSchemaComment } from 'stripe-experiment-sync/supabase'
+import { SchemaInstallationStatus, StripeSchemaComment } from '@stripe/sync-engine/supabase'
import type {
StripeSyncState,
diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts
index afcffdc5029..d627b929781 100644
--- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts
+++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts
@@ -1,5 +1,5 @@
+import { getCurrentVersion, parseSchemaComment } from '@stripe/sync-engine/supabase'
import { useEffect } from 'react'
-import { getCurrentVersion, parseSchemaComment } from 'stripe-experiment-sync/supabase'
import {
findStripeSchema,
diff --git a/apps/studio/components/interfaces/JwtSecrets/illustrations.tsx b/apps/studio/components/interfaces/JwtSecrets/illustrations.tsx
deleted file mode 100644
index f261c0bdc5f..00000000000
--- a/apps/studio/components/interfaces/JwtSecrets/illustrations.tsx
+++ /dev/null
@@ -1,384 +0,0 @@
-export const StandbyKeyIllustration = () => (
-
-)
-
-export const WhyRotateKeysIllustration = () => (
-
-)
-
-export const WhyUseStandbyKeysIllustration = () => (
-
-)
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx
index dc9ddcb8c46..11b2bf2fb27 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx
@@ -147,9 +147,10 @@ export const Restriction = () => {
{dayjs(org.restriction_data['grace_period_end']).format('DD MMM, YYYY')}
- . Fair Use Policy applies now. Stay below your plan's quota or{' '}
- {org.plan.id === 'free' ? 'upgrade your plan' : 'disable spend cap'} if you expect to
- exceed it. If you exceed your quota, requests will respond with a 402 status code.
+ . Fair Use Policy applies now. If your organization is over its quota, your projects
+ can be restricted and requests will respond with a 402 status code.{' '}
+ {org.plan.id === 'free' ? 'Upgrade your plan' : 'Disable spend cap'} if you expect to
+ exceed your plan's quota.
@@ -187,9 +188,9 @@ export const Restriction = () => {
serve requests and will respond with a 402 status code. You have exceeded your plan's
quota{violationLabels && ` ${violationLabels}`}.{' '}
{org.plan.id === 'free' ? 'Upgrade your plan' : 'Disable spend cap'} to lift
- restrictions immediately, or wait until your quota refills at the start of your next
- billing period. Note that there may be a short delay after your billing period resets
- before restrictions are fully lifted.
+ restrictions immediately, or wait until the start of your next billing period. Note
+ that there may be a short delay after your billing period resets before restrictions
+ are fully lifted.
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx
index b64817dc36d..c1b4288de28 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/CancellationFlow.tsx
@@ -1,6 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
-import React, { ComponentProps, useEffect, useMemo, useState } from 'react'
+import { ComponentProps, useEffect, useMemo, useState } from 'react'
import { DowngradeModal } from './DowngradeModal'
import { ExitSurveyModal } from './ExitSurveyModal'
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx
index 70197a0f582..60b5714cf9a 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/DowngradeModal.tsx
@@ -2,14 +2,13 @@ import { MinusCircle, PauseCircle } from 'lucide-react'
import { useMemo } from 'react'
import { plans as subscriptionsPlans } from 'shared-data/plans'
import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
+ Button,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogSectionSeparator,
+ DialogTitle,
} from 'ui'
import { Admonition } from 'ui-patterns'
@@ -96,96 +95,93 @@ export const DowngradeModal = ({
)
return (
-
-
-
- {`Confirm to downgrade to ${selectedPlan?.name} plan`}
-
-
-
-
+
+ Confirm to downgrade to {selectedPlan?.name} plan
+
+
+
- Connect a GitHub repository to a Supabase project. The GitHub app watches file,
- branch, and pull request activity in your repository.
-
-
-
-
-
- {connections?.map((connection) => (
-
- ))}
-
-
-
- Add new project connection
-
-
-
- {isGitHubAuthorized && (
-
- You are authorized with the Supabase GitHub App. You can configure your{' '}
-
- GitHub App installations and repository access
-
- , or{' '}
-
- revoke your authorization
-
- .
-
{availableActions.map((action) => (
{
{
key: 'realtime',
title: 'Realtime requests',
+ href: `/project/${projectRef}/realtime/inspector`,
route: '/logs/realtime-logs',
enabled: true,
},
diff --git a/apps/studio/components/interfaces/ProjectHome/ProjectUsageSectionDeltas.tsx b/apps/studio/components/interfaces/ProjectHome/ProjectUsageSectionDeltas.tsx
index 60a8268f1a6..b70c9f85964 100644
--- a/apps/studio/components/interfaces/ProjectHome/ProjectUsageSectionDeltas.tsx
+++ b/apps/studio/components/interfaces/ProjectHome/ProjectUsageSectionDeltas.tsx
@@ -76,6 +76,7 @@ export const ProjectUsageSectionDeltas = () => {
{
key: 'functions',
title: 'Edge Functions',
+ href: `/project/${projectRef}/functions`,
route: '/logs/edge-functions-logs',
enabled: true,
},
@@ -96,12 +97,14 @@ export const ProjectUsageSectionDeltas = () => {
{
key: 'realtime',
title: 'Realtime',
+ href: `/project/${projectRef}/realtime/inspector`,
route: '/logs/realtime-logs',
enabled: true,
},
{
key: 'data_api',
title: 'API Gateway',
+ href: `/project/${projectRef}/integrations/data_api/overview`,
route: '/logs/edge-logs',
enabled: dataApiEnabled,
},
diff --git a/apps/studio/components/interfaces/Realtime/Inspector/SendMessageModal.tsx b/apps/studio/components/interfaces/Realtime/Inspector/SendMessageModal.tsx
index 7f00e199f5b..24d0b79b232 100644
--- a/apps/studio/components/interfaces/Realtime/Inspector/SendMessageModal.tsx
+++ b/apps/studio/components/interfaces/Realtime/Inspector/SendMessageModal.tsx
@@ -12,7 +12,7 @@ import {
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
-import CodeEditor from '@/components/ui/CodeEditor/CodeEditor'
+import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
import { tryParseJson } from '@/lib/helpers'
interface SendMessageModalProps {
diff --git a/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx b/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx
index 2d8df00e0e2..dc954c5088a 100644
--- a/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx
@@ -1,16 +1,16 @@
-import Editor, { Monaco, OnMount } from '@monaco-editor/react'
+import { Monaco, OnMount } from '@monaco-editor/react'
import { useDebounce } from '@uidotdev/usehooks'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
+import { noop } from 'lodash'
import { useRouter } from 'next/router'
-import { MutableRefObject, useEffect, useRef, useState } from 'react'
-import { cn } from 'ui'
+import { RefObject, useEffect, useRef, useState } from 'react'
import { Admonition } from 'ui-patterns'
-import { useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
import type { IStandaloneCodeEditor } from './SQLEditor.types'
import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { getEditorSelectionParts } from '@/components/ui/AIEditor/utils'
+import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useProfile } from '@/lib/profile'
@@ -19,17 +19,19 @@ import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
+import { canEditSnippet } from '@/state/sql-editor/sql-editor-rules'
import { useTabsStateSnapshot } from '@/state/tabs'
export type MonacoEditorProps = {
id: string
snippetName: string
className?: string
- editorRef: MutableRefObject
- monacoRef: MutableRefObject
+ editorRef: RefObject
+ monacoRef: RefObject
autoFocus?: boolean
executeQuery: () => void
executeExplainQuery: () => void
+ showExplainAction?: boolean
prettifyQuery: () => void
onHasSelection: (value: boolean) => void
onMount?: (editor: IStandaloneCodeEditor) => void
@@ -43,7 +45,7 @@ export type MonacoEditorProps = {
placeholder?: string
}
-const MonacoEditor = ({
+export const MonacoEditor = ({
id,
snippetName,
editorRef,
@@ -53,6 +55,7 @@ const MonacoEditor = ({
className,
executeQuery,
executeExplainQuery,
+ showExplainAction = true,
prettifyQuery,
onHasSelection,
onPrompt,
@@ -72,20 +75,12 @@ const MonacoEditor = ({
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
true
)
- const isAIAssistantHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE)
- const isCommandMenuHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.COMMAND_MENU_OPEN)
- const setCommandMenuOpen = useSetCommandMenuOpen()
- // [Joshen] Lodash debounce doesn't seem to be working here, so opting to use useDebounce
const [value, setValue] = useState('')
const debouncedValue = useDebounce(value, 1000)
const snippet = snapV2.snippets[id]
- const disableEdit =
- snippet?.snippet.visibility === 'project' && snippet?.snippet.owner_id !== profile?.id
-
- const executeQueryRef = useRef(executeQuery)
- executeQueryRef.current = executeQuery
+ const disableEdit = !!snippet && !canEditSnippet(snippet.snippet, profile?.id)
const executeExplainQueryRef = useRef(executeExplainQuery)
executeExplainQueryRef.current = executeExplainQuery
@@ -93,22 +88,14 @@ const MonacoEditor = ({
const prettifyQueryRef = useRef(prettifyQuery)
prettifyQueryRef.current = prettifyQuery
+ const isAIAssistantHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE)
const aiHotkeyEnabledRef = useRef(isAIAssistantHotkeyEnabled)
aiHotkeyEnabledRef.current = isAIAssistantHotkeyEnabled
- const commandMenuHotkeyEnabledRef = useRef(isCommandMenuHotkeyEnabled)
- commandMenuHotkeyEnabledRef.current = isCommandMenuHotkeyEnabled
-
- const setCommandMenuOpenRef = useRef(setCommandMenuOpen)
- setCommandMenuOpenRef.current = setCommandMenuOpen
-
- const handleEditorOnMount: OnMount = async (editor, monaco) => {
- editorRef.current = editor
- monacoRef.current = monaco
-
- const model = editorRef.current.getModel()
+ const handleEditorOnMount: OnMount = (editor, monaco) => {
+ const model = editor.getModel()
if (model !== null) {
- monacoRef.current.editor.setModelMarkers(model, 'owner', [])
+ monaco.editor.setModelMarkers(model, 'owner', [])
}
// Blur the editor on Escape so users can hop out to the rest of the UI.
@@ -134,27 +121,18 @@ const MonacoEditor = ({
].join(' && ')
)
- editor.addAction({
- id: 'run-query',
- label: 'Run Query',
- keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter],
- contextMenuGroupId: 'operation',
- contextMenuOrder: 0,
- run: () => {
- executeQueryRef.current()
- },
- })
-
- editor.addAction({
- id: 'run-explain-query',
- label: 'Run EXPLAIN ANALYZE',
- keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter],
- contextMenuGroupId: 'operation',
- contextMenuOrder: 1,
- run: () => {
- executeExplainQueryRef.current()
- },
- })
+ if (showExplainAction) {
+ editor.addAction({
+ id: 'run-explain-query',
+ label: 'Run EXPLAIN ANALYZE',
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter],
+ contextMenuGroupId: 'operation',
+ contextMenuOrder: 1,
+ run: () => {
+ executeExplainQueryRef.current()
+ },
+ })
+ }
editor.addAction({
id: 'save-query',
@@ -184,13 +162,15 @@ const MonacoEditor = ({
contextMenuGroupId: 'operation',
contextMenuOrder: 1,
run: () => {
- const selectedValue = (editorRef?.current as any)
- .getModel()
- .getValueInRange((editorRef?.current as any)?.getSelection())
+ const selection = editorRef?.current?.getSelection()
+ if (!selection) return
+
+ const selectedValue = editorRef?.current?.getModel()?.getValueInRange(selection)
+
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
aiSnap.newChat({
name: 'Explain code section',
- sqlSnippets: [selectedValue],
+ sqlSnippets: [selectedValue ?? ''],
initialInput: 'Can you explain this section to me in more detail?',
})
},
@@ -219,15 +199,6 @@ const MonacoEditor = ({
})
}
- // Monaco claims Cmd+K as a chord prefix, which swallows the global command
- // menu shortcut while the editor is focused. Intercept it here and open the
- // command menu directly so it works the same inside and outside the editor.
- editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, () => {
- if (commandMenuHotkeyEnabledRef.current) {
- setCommandMenuOpenRef.current(true)
- }
- })
-
editor.onDidChangeCursorSelection(({ selection }) => {
const noSelection =
selection.startLineNumber === selection.endLineNumber &&
@@ -235,11 +206,6 @@ const MonacoEditor = ({
onHasSelection(!noSelection)
})
- if (autoFocus) {
- if (editor.getValue().length === 1) editor.setPosition({ lineNumber: 1, column: 2 })
- editor.focus()
- }
-
onMount?.(editor)
}
@@ -255,7 +221,15 @@ const MonacoEditor = ({
project_id: project?.id,
})
snapV2.addSnippet({ projectRef: ref, snippet })
- router.push(`/project/${ref}/sql/${snippet.id}`, undefined, { shallow: true })
+ // When the editor was seeded from a `content` deep-link, replace rather
+ // than push. The caller navigated to `/sql/new?content=...` (a long,
+ // one-shot URL); replacing collapses it out of history so Back returns to
+ // the originating page instead of a wasted step that re-seeds the snippet.
+ if (router.query.content !== undefined) {
+ router.replace(`/project/${ref}/sql/${snippet.id}`, undefined, { shallow: true })
+ } else {
+ router.push(`/project/${ref}/sql/${snippet.id}`, undefined, { shallow: true })
+ }
}
setValue(value)
}
@@ -285,29 +259,26 @@ const MonacoEditor = ({
description="This snippet has been shared to the project and is only editable by the owner who created this snippet. You may duplicate this snippet into a personal copy by right clicking on the snippet and selecting “Duplicate query”."
/>
)}
-
>
)
}
-
-export default MonacoEditor
diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx
index 5a16a2e14cb..c3f736f8caf 100644
--- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx
@@ -96,7 +96,10 @@ import { getSqlEditorV2StateSnapshot, useSqlEditorV2StateSnapshot } from '@/stat
import { createTabId, useTabsStateSnapshot } from '@/state/tabs'
// Load the monaco editor client-side only (does not behave well server-side)
-const MonacoEditor = dynamic(() => import('./MonacoEditor'), { ssr: false })
+const MonacoEditor = dynamic(
+ () => import('./MonacoEditor').then(({ MonacoEditor }) => MonacoEditor),
+ { ssr: false }
+)
const DiffEditor = dynamic(
() => import('../../ui/DiffEditor').then(({ DiffEditor }) => DiffEditor),
{ ssr: false }
@@ -121,6 +124,9 @@ export const SQLEditor = () => {
const { aiOptInLevel } = useOrgAiOptInLevel()
const showPrettyExplain = useFlag('ShowPrettyExplain')
+ // [Ali] Kill switch to hide the SQL Editor Explain tab and its entry points
+ const disablePrettyExplain = useFlag('DisablePrettyExplainOnSqlEditor')
+
const {
sourceSqlDiff,
setSourceSqlDiff,
@@ -223,7 +229,7 @@ export const SQLEditor = () => {
if (id) {
snapV2.addResult(id, data.result, vars.autoLimit)
- if (showPrettyExplain && isExplainQuery(data.result)) {
+ if (!disablePrettyExplain && showPrettyExplain && isExplainQuery(data.result)) {
snapV2.addExplainResult(id, data.result)
setActiveUtilityTab('explain')
} else if (activeUtilityTab === 'explain') {
@@ -535,6 +541,7 @@ export const SQLEditor = () => {
])
useShortcut(SHORTCUT_IDS.SQL_EDITOR_EXPLAIN, executeExplainQuery, {
+ enabled: !disablePrettyExplain,
registerInCommandMenu: true,
})
@@ -958,6 +965,7 @@ export const SQLEditor = () => {
monacoRef={monacoRef}
executeQuery={executeQuery}
executeExplainQuery={executeExplainQuery}
+ showExplainAction={!disablePrettyExplain}
prettifyQuery={prettifyQuery}
onHasSelection={setHasSelection}
onMount={onMount}
@@ -1022,6 +1030,7 @@ export const SQLEditor = () => {
prettifyQuery={prettifyQuery}
executeQuery={executeQueryFromButton}
executeExplainQuery={executeExplainQuery}
+ showExplainTab={!disablePrettyExplain}
onDebug={onDebug}
buildDebugPrompt={buildDebugPrompt}
activeTab={activeUtilityTab}
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/CellDetailPanel.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/CellDetailPanel.tsx
index e969b189f38..1ef52e82351 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/CellDetailPanel.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/CellDetailPanel.tsx
@@ -3,7 +3,7 @@ import remarkGfm from 'remark-gfm'
import { Sheet, SheetContent, SheetHeader, SheetTitle } from 'ui'
import { Markdown } from '@/components/interfaces/Markdown'
-import CodeEditor from '@/components/ui/CodeEditor/CodeEditor'
+import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
import { TwoOptionToggle } from '@/components/ui/TwoOptionToggle'
interface CellDetailPanelProps {
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ReadOnlyBadge.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ReadOnlyBadge.tsx
index 7a2a8aa75d0..253675db650 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ReadOnlyBadge.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ReadOnlyBadge.tsx
@@ -2,6 +2,7 @@ import { Badge } from 'ui'
import { useProfile } from '@/lib/profile'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
+import { isSnippetOwner } from '@/state/sql-editor/sql-editor-rules'
export type ReadOnlyBadgeProps = { id: string }
const ReadOnlyBadge = ({ id }: ReadOnlyBadgeProps) => {
@@ -9,9 +10,9 @@ const ReadOnlyBadge = ({ id }: ReadOnlyBadgeProps) => {
const snapV2 = useSqlEditorV2StateSnapshot()
const snippet = snapV2.snippets[id]
- const isSnippetOwner = profile?.id === snippet?.snippet.owner_id
+ const snippetIsOwned = !!snippet && isSnippetOwner(snippet.snippet, profile?.id)
- return <>{isSnippetOwner ? null : Read-only}>
+ return <>{snippetIsOwned ? null : Read-only}>
}
export default ReadOnlyBadge
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SavingIndicator.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SavingIndicator.tsx
index 26f92e1b712..12c34a4214d 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SavingIndicator.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SavingIndicator.tsx
@@ -6,6 +6,7 @@ import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import ReadOnlyBadge from './ReadOnlyBadge'
import { useProfile } from '@/lib/profile'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
+import { isSnippetOwner } from '@/state/sql-editor/sql-editor-rules'
export type SavingIndicatorProps = { id: string }
@@ -18,7 +19,7 @@ const SavingIndicator = ({ id }: SavingIndicatorProps) => {
const [showSavedText, setShowSavedText] = useState(false)
const snippet = snapV2.snippets[id]
- const isSnippetOwner = profile?.id === snippet?.snippet.owner_id
+ const snippetIsOwned = !!snippet && isSnippetOwner(snippet.snippet, profile?.id)
useEffect(() => {
let cancel = false
@@ -40,7 +41,7 @@ const SavingIndicator = ({ id }: SavingIndicatorProps) => {
return (
<>
- {isSnippetOwner && savingState === 'UPDATING_FAILED' && (
+ {snippetIsOwned && savingState === 'UPDATING_FAILED' && (
{
Saving changes...
) : savingState === 'UPDATING_FAILED' ? (
- isSnippetOwner ? (
+ snippetIsOwned ? (
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
index 662dec0fc50..d8ab07d67c4 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
@@ -22,6 +22,7 @@ export type UtilityPanelProps = {
prettifyQuery: () => void
executeQuery: () => void
executeExplainQuery: () => void
+ showExplainTab?: boolean
onDebug: () => void
buildDebugPrompt: () => string
activeTab?: string
@@ -47,6 +48,7 @@ export const UtilityPanel = ({
prettifyQuery,
executeQuery,
executeExplainQuery,
+ showExplainTab = true,
onDebug,
buildDebugPrompt,
activeTab = 'results',
@@ -136,9 +138,11 @@ export const UtilityPanel = ({
Results
-
- Explain
-
+ {showExplainTab && (
+
+ Explain
+
+ )}
Chart
@@ -177,9 +181,11 @@ export const UtilityPanel = ({
/>
-
-
-
+ {showExplainTab && (
+
+
+
+ )}
diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionLogging.tsx b/apps/studio/components/interfaces/Settings/Database/ConnectionLogging.tsx
new file mode 100644
index 00000000000..cf30774357b
--- /dev/null
+++ b/apps/studio/components/interfaces/Settings/Database/ConnectionLogging.tsx
@@ -0,0 +1,164 @@
+import { zodResolver } from '@hookform/resolvers/zod'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { useQuery } from '@tanstack/react-query'
+import { useParams } from 'common'
+import { useEffect, useMemo } from 'react'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import { Button, Card, CardContent, CardFooter, Form, FormControl, FormField, Switch } from 'ui'
+import {
+ PageSection,
+ PageSectionAside,
+ PageSectionContent,
+ PageSectionMeta,
+ PageSectionSummary,
+ PageSectionTitle,
+} from 'ui-patterns'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import z from 'zod'
+
+import AlertError from '@/components/ui/AlertError'
+import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
+import { DocsButton } from '@/components/ui/DocsButton'
+import { usePostgresConfigurationUpdateMutation } from '@/data/config/postgres-config-mutation'
+import { postgresConfigurationQueryOptions } from '@/data/config/postgres-config-query'
+import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+import { DOCS_URL } from '@/lib/constants'
+
+const FormSchema = z.object({
+ log_connections: z.boolean(),
+ log_disconnections: z.boolean(),
+})
+
+export const ConnectionLogging = () => {
+ const { ref: projectRef } = useParams()
+ const { data: project } = useSelectedProjectQuery()
+ const { can: canUpdatePostgresConfiguration } = useAsyncCheckPermissions(
+ PermissionAction.UPDATE,
+ 'projects',
+ { resource: { project_id: project?.id } }
+ )
+
+ const {
+ data: postgresConfig,
+ error,
+ isSuccess,
+ isError,
+ } = useQuery(postgresConfigurationQueryOptions({ projectRef }))
+
+ const { mutate: updatePostgresConfig, isPending: isSaving } =
+ usePostgresConfigurationUpdateMutation({
+ onSuccess: () => toast('Successfully updated logging settings'),
+ })
+
+ const defaultValues = useMemo(
+ () => ({
+ log_connections: postgresConfig?.log_connections ?? false,
+ log_disconnections: postgresConfig?.log_disconnections ?? false,
+ }),
+ [postgresConfig]
+ )
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ values: defaultValues,
+ })
+ const hasChanges = form.formState.isDirty
+
+ const onSubmit: SubmitHandler> = (payload) => {
+ if (!projectRef) return
+ updatePostgresConfig({ projectRef, payload })
+ }
+
+ useEffect(() => {
+ if (isSuccess) form.reset(defaultValues)
+ }, [isSuccess, defaultValues, form])
+
+ return (
+
+
+
+ Connection logging
+
+
+
+
+
+
+ {isError ? (
+
+ ) : (
+
+
+ )}
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Settings/Database/Database.constants.ts b/apps/studio/components/interfaces/Settings/Database/Database.constants.ts
deleted file mode 100644
index 6318c6148c9..00000000000
--- a/apps/studio/components/interfaces/Settings/Database/Database.constants.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export const TRANSACTION_MODE_DESCRIPTION =
- 'Connection is assigned to the client for the duration of a transaction. Some session-based Postgres features such as prepared statements are not available with this option.'
-
-export const SESSION_MODE_DESCRIPTION =
- 'When a new client connects, a connection is assigned to the client until it disconnects. All Postgres features can be used with this option.'
diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx
index f31820fbb2a..79de0684859 100644
--- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx
+++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx
@@ -10,7 +10,7 @@ import {
import { ResetDbPasswordDialog } from './ResetDbPasswordDialog'
-const ResetDbPassword = ({ disabled = false }) => {
+export const ResetDbPassword = ({ disabled = false }) => {
return (
diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/DeleteCustomDomainConfirmationDialog.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/DeleteCustomDomainConfirmationDialog.tsx
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx
index 669a0d23d84..db1ca7378d6 100644
--- a/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx
+++ b/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx
@@ -160,6 +160,10 @@ const getWarningTitle = (warning: ProjectUpgradeEligibilityWarning): string => {
switch (warning.type) {
case 'pg_graphql_introspection_change':
return 'GraphQL introspection will be disabled by default after upgrade'
+ case 'ltree_reindex_required':
+ return 'ltree indexes must be reindexed after this upgrade'
+ case 'operator_estimator_gate':
+ return 'Custom operators may need attention after this upgrade'
}
}
@@ -167,6 +171,10 @@ const getWarningDescription = (warning: ProjectUpgradeEligibilityWarning): strin
switch (warning.type) {
case 'pg_graphql_introspection_change':
return 'After upgrading, queries to `__schema` and `__type` will return an error unless introspection is explicitly re-enabled on the schema. Regular data queries are not affected.'
+ case 'ltree_reindex_required':
+ return 'After upgrading, ltree indexes on this database can return incomplete results until they are rebuilt. Run `REINDEX INDEX CONCURRENTLY` on the affected indexes — this runs online with no downtime.'
+ case 'operator_estimator_gate':
+ return 'After upgrading, recreating an operator that references a non-built-in selectivity estimator (for example during a restore or branch) requires superuser and may fail. Most projects are not affected.'
}
}
@@ -174,6 +182,10 @@ const getWarningLink = (warning: ProjectUpgradeEligibilityWarning): string => {
switch (warning.type) {
case 'pg_graphql_introspection_change':
return `${DOCS_URL}/guides/platform/upgrading#upgrading-to-pg_graphql-160`
+ case 'ltree_reindex_required':
+ return `${DOCS_URL}/guides/platform/upgrading#ltree-indexes-require-reindexing-after-upgrade`
+ case 'operator_estimator_gate':
+ return `${DOCS_URL}/guides/platform/upgrading#custom-operator-selectivity-estimators`
}
}
diff --git a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx
index 44ad75a9c30..1f181eb7617 100644
--- a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx
+++ b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx
@@ -1,6 +1,8 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
-import { useMemo } from 'react'
+import { useRouter } from 'next/router'
+import { useCallback, useMemo } from 'react'
+import { toast } from 'sonner'
import {
PageSection,
PageSectionContent,
@@ -13,39 +15,116 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { IntegrationSectionIcon } from '../IntegrationsSettings'
import { GitHubIntegrationConnectionForm } from './GitHubIntegrationConnectionForm'
+import { IntegrationConnectionItem } from '@/components/interfaces/Integrations/VercelGithub/IntegrationConnection'
+import { EmptyIntegrationConnection } from '@/components/interfaces/Integrations/VercelGithub/IntegrationPanels'
+import { InlineLink } from '@/components/ui/InlineLink'
import NoPermission from '@/components/ui/NoPermission'
-import { useGitHubConnectionsQuery } from '@/data/integrations/github-connections-query'
+import { useGitHubAuthorizationQuery } from '@/data/integrations/github-authorization-query'
+import { useGitHubConnectionDeleteMutation } from '@/data/integrations/github-connection-delete-mutation'
+import {
+ useGitHubConnectionsQuery,
+ type GitHubConnection,
+} from '@/data/integrations/github-connections-query'
+import type { IntegrationProjectConnection } from '@/data/integrations/integrations.types'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
+import {
+ GITHUB_INTEGRATION_INSTALLATION_URL,
+ GITHUB_INTEGRATION_REVOKE_AUTHORIZATION_URL,
+} from '@/lib/github'
+import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
+import { useShortcut } from '@/state/shortcuts/useShortcut'
-export const GitHubSection = () => {
+const toIntegrationProjectConnection = (
+ connection: GitHubConnection
+): IntegrationProjectConnection => ({
+ id: String(connection.id),
+ added_by: {
+ id: String(connection.user?.id),
+ primary_email: connection.user?.primary_email ?? '',
+ username: connection.user?.username ?? '',
+ },
+ foreign_project_id: String(connection.repository.id),
+ supabase_project_ref: connection.project.ref,
+ organization_integration_id: 'unused',
+ inserted_at: connection.inserted_at,
+ updated_at: connection.updated_at,
+ metadata: {
+ name: connection.repository.name,
+ } as IntegrationProjectConnection['metadata'],
+})
+
+export const GitHubSection = ({ isProjectScoped }: { isProjectScoped: boolean }) => {
+ const router = useRouter()
const { ref: projectRef } = useParams()
- const { data: organization } = useSelectedOrganizationQuery()
+ const { data: org } = useSelectedOrganizationQuery()
const { can: canReadGitHubConnection, isLoading: isLoadingPermissions } =
useAsyncCheckPermissions(PermissionAction.READ, 'integrations.github_connections')
-
- const { data: connections } = useGitHubConnectionsQuery(
- { organizationId: organization?.id },
- { enabled: !!projectRef && !!organization?.id }
+ const { can: canCreateGitHubConnection } = useAsyncCheckPermissions(
+ PermissionAction.CREATE,
+ 'integrations.github_connections'
)
+ const { can: canUpdateGitHubConnection } = useAsyncCheckPermissions(
+ PermissionAction.UPDATE,
+ 'integrations.github_connections'
+ )
+
+ const { data: gitHubAuthorization } = useGitHubAuthorizationQuery({
+ enabled: !isProjectScoped,
+ })
+ const { data: connections } = useGitHubConnectionsQuery(
+ { organizationId: org?.id },
+ { enabled: isProjectScoped ? !!projectRef && !!org?.id : !!org?.id }
+ )
+
+ const { mutate: deleteGitHubConnection } = useGitHubConnectionDeleteMutation({
+ onSuccess: () => {
+ toast.success('Successfully deleted GitHub connection')
+ },
+ })
const existingConnection = useMemo(
() => connections?.find((c) => c.project.ref === projectRef),
[connections, projectRef]
)
+ const onAddGitHubConnection = useCallback(() => {
+ router.push('/project/_/settings/integrations')
+ }, [router])
+
+ useShortcut(SHORTCUT_IDS.ORG_INTEGRATIONS_ADD_CONNECTION, onAddGitHubConnection, {
+ enabled: !isProjectScoped && canCreateGitHubConnection,
+ })
+
+ const onDeleteGitHubConnection = useCallback(
+ async (connection: IntegrationProjectConnection) => {
+ if (!org?.id) {
+ toast.error('Organization not found')
+ return
+ }
+
+ deleteGitHubConnection({
+ connectionId: connection.id,
+ organizationId: org.id,
+ })
+ },
+ [deleteGitHubConnection, org?.id]
+ )
+
return (
- GitHub Integration
+
+ {isProjectScoped ? 'GitHub Integration' : 'GitHub Connections'}
+
- Connect any of your GitHub repositories to a project. Supabase applies database
- changes when you merge into your production branch. If branching is enabled, each pull
- request gets its own preview database.
+ {isProjectScoped
+ ? 'Connect any of your GitHub repositories to a project. Supabase applies database changes when you merge into your production branch. If branching is enabled, each pull request gets its own preview database.'
+ : 'Connect any of your GitHub repositories to a project. The GitHub app watches file, branch, and pull request activity in your repository.'}
+ You are authorized with the Supabase GitHub App. You can configure your{' '}
+
+ GitHub App installations and repository access
+
+ , or{' '}
+
+ revoke your authorization
+
+ .
+
+
+
+
+ )}
+
+ {/* Only show load more section if we have rows OR if we're not in initial loading state */}
+ {(rows.length > 0 || (!isLoading && !rows.length)) && (
+
+
+