mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
Multigres (high-availability) projects only accept TLS connections with direct SSL negotiation, and they don't support connection pooling at all — neither Supavisor nor the dedicated PgBouncer pooler exists for them. Studio previously showed pooler connection strings that would fail with "server closed the connection unexpectedly". This PR makes every connection-string surface direct-only for HA projects and appends `?sslmode=require&sslnegotiation=direct` to the examples. Non-HA projects are unchanged. Addresses [FE-4019](https://linear.app/supabase/issue/FE-4019/append-ssl-params-to-multigres-connection-string-examples-in-ui) **Changed:** - `buildConnectionStringPooler` gets an HA branch that collapses every slot in the bag to the direct connection string with the SSL params appended (mirroring the existing CLI branch, which also has no pooler) — dedicated slots come back `undefined` and `ipv4SupportedForDedicatedPooler` is forced off. Since HA never reaches the pooler layout anymore, the earlier per-URI SSL-append logic on pooler strings is removed - `useConnectState` coerces `connectionMethod` to `direct` and `useSharedPooler` to `false` for HA projects. The Connect sheet restores the last-used method from localStorage shared across projects, so a "Transaction pooler" selection made on a regular project could otherwise leak pooler-flavored notices, badges, and telemetry into an HA project - Prisma and Drizzle ORM tabs get an HA branch: `DATABASE_URL`/`DIRECT_URL` both use the direct connection, no `?pgbouncer=true` appended, with a comment explaining Multigres doesn't support pooling. The 5-arm nested ternaries in both files are flattened into `getEnvCode` helpers that switch on a shared `resolveOrmConnectionScenario` helper (`OrmConnection.utils.ts`), so the deployment-mode/HA branching lives in one tested place and each file keeps only its own formatting - The PgBouncer and Supavisor config queries are disabled (`enabled: !isHighAvailability`) in the Connect sheet — those endpoints serve pooler config that doesn't exist on Multigres - `parseConnectionParams` keeps the URI's query string in a new `search` field so formats rebuilt from parsed parts can carry it - psql switches from the `-h/-p/-d/-U` flag form to the quoted-URI form when query params are present (flags can't express them; psql still prompts for the password) - JDBC appends the params using pgJDBC's casing (`sslNegotiation`, supported since 42.7.4) - Prisma's `?pgbouncer=true` appends are query-aware (join with `&` when the URI already has a query string) via a new `appendConnectionStringParams` helper - The project home "Direct connection string" copy item also appends the params for HA projects **Added:** - Unit tests for the HA collapse behavior (all slots direct, dedicated config and IPv4 add-on ignored, no SSL params on non-HA output), the `useConnectState` coercion, the psql/JDBC builders (moved from `content.tsx` into `ConnectionString.utils.ts` so they're testable), and `resolveOrmConnectionScenario` (every deployment-mode/HA/pooler branch) **Known gaps (left out deliberately):** - The grid ExportDialog psql/pg_dump commands, the .NET `appsettings.json` (Npgsql only supports direct negotiation from v9 via `SSL Negotiation=Direct`), and the SQLAlchemy keyword-style `.env` are flag/keyword forms that can't carry the URI params — these would still fail against Multigres and need a follow-up - Settings > Database's Connection Pooling section and the pooler logs page have no HA gating yet — they'd still render pooler config UI for a Multigres project and should be hidden in a follow-up ## To test On a **Multigres (HA) project** (staging only supports `us-east-1` for Multigres): - Open the Connect sheet → Direct tab: there's no connection-method picker, and the connection string is the direct one ending with `?sslmode=require&sslnegotiation=direct` for the URI, PHP, and psql (quoted-URI form) types; JDBC includes `&sslmode=require&sslNegotiation=direct` - ORM tab → Prisma: both `DATABASE_URL` and `DIRECT_URL` are the direct connection string with the SSL params, no `pgbouncer=true`, with a "Multigres does not support connection pooling" comment. Drizzle likewise shows the direct string only - Framework tabs (e.g. Next.js): every `DATABASE_URL` carries the direct string with the params exactly once - Open the network tab: no requests to `/config/pgbouncer` or `/config/supavisor` while using the Connect sheet - To check the localStorage coercion: on a **regular** project pick "Transaction pooler" in the Connect sheet, then open the sheet on the Multigres project — no pooler badge/notices, string is still direct - Copy the URI, substitute your password, and `psql "<string>"` — it should connect - Project home → Copy dropdown → "Direct connection string" includes the params On a **regular (non-Multigres) project** — confirm nothing changed: - Connect sheet: direct/session/transaction strings for all connection types (URI, psql flag form, JDBC, PHP) look the same as before, no SSL params appended - Prisma/Drizzle tabs render identically (`?pgbouncer=true` still appended with `?`, dedicated-pooler alternatives still shown per IPv4 add-on state) - Project home copy dropdown is unchanged <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Enhanced connection-string generation for high-availability projects, including required SSL settings for direct connections. * Preserved URI query parameters in PostgreSQL, `psql`, JDBC, and generated environment configurations. * Improved ORM environment templates with clearer handling for pooler and high-availability connection scenarios. * **Bug Fixes** * High-availability projects now consistently use direct connections instead of pooler options. * Connection strings and generated templates update correctly when availability settings change. * **Tests** * Expanded coverage for query parameters, high-availability behavior, and connection scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
345 lines
12 KiB
TypeScript
345 lines
12 KiB
TypeScript
import { useParams } from 'common'
|
|
import dynamic from 'next/dynamic'
|
|
import Link from 'next/link'
|
|
import { useMemo, useRef, type ComponentType } from 'react'
|
|
import { Button } from 'ui'
|
|
import { Admonition } from 'ui-patterns/Admonition'
|
|
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import type {
|
|
ConnectionStringPooler,
|
|
ConnectState,
|
|
DeploymentMode,
|
|
ProjectKeys,
|
|
ResolvedStep,
|
|
StepContentProps,
|
|
} from './Connect.types'
|
|
import { ConnectSheetStep } from './ConnectSheetStep'
|
|
import {
|
|
resolveContentPath,
|
|
shouldFetchDataApiConfig,
|
|
shouldShowDataApiDisabledWarning,
|
|
shouldShowIpv4AddonNotice,
|
|
shouldShowSelfHostedMcpNotice,
|
|
shouldShowSessionPoolerNotice,
|
|
} from './ConnectStepsSection.utils'
|
|
import { CopyPromptButton } from './CopyPromptAdmonition'
|
|
import { buildConnectionStringPooler, getConnectionStrings } from './DatabaseSettings.utils'
|
|
import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
|
|
import { DocsButton } from '@/components/ui/DocsButton'
|
|
import { InlineLink } from '@/components/ui/InlineLink'
|
|
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
|
|
import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query'
|
|
import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query'
|
|
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
|
|
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
|
import { useDeploymentMode } from '@/hooks/misc/useDeploymentMode'
|
|
import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled'
|
|
import { useIsHighAvailability } from '@/hooks/misc/useSelectedProject'
|
|
import { DOCS_URL } from '@/lib/constants'
|
|
import { pluckObjectFields } from '@/lib/helpers'
|
|
|
|
interface ConnectStepsSectionProps {
|
|
steps: ResolvedStep[]
|
|
state: ConnectState
|
|
projectKeys: ProjectKeys
|
|
}
|
|
|
|
/**
|
|
* Hook to fetch and prepare connection strings for step content.
|
|
*/
|
|
function useConnectionStringPooler(deploymentMode: DeploymentMode): ConnectionStringPooler {
|
|
const { ref: projectRef } = useParams()
|
|
const { hasAccess: allowPgBouncerSelection } = useCheckEntitlements('dedicated_pooler')
|
|
const isHighAvailability = useIsHighAvailability()
|
|
|
|
const { data: settings } = useProjectSettingsV2Query({ projectRef })
|
|
// Multigres has no pooler, so the pooler config endpoints don't apply
|
|
const { data: pgbouncerConfig } = usePgbouncerConfigQuery(
|
|
{ projectRef },
|
|
{ enabled: !isHighAvailability }
|
|
)
|
|
const { data: supavisorConfig } = useSupavisorConfigurationQuery(
|
|
{ projectRef },
|
|
{ enabled: !isHighAvailability }
|
|
)
|
|
const { data: addons } = useProjectAddonsQuery({ projectRef })
|
|
const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? [])
|
|
|
|
// Each intermediate derived value is memoized so its reference stays stable
|
|
// across renders while the upstream query data is unchanged. Without this,
|
|
// pluckObjectFields / getConnectionStrings would mint fresh objects every
|
|
// render and invalidate the final useMemo on each tick (and ripple through
|
|
// every consumer that lists ConnectionStringPooler in their own deps).
|
|
const connectionInfo = useMemo(() => {
|
|
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
|
|
const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
|
|
return pluckObjectFields(settings || emptyState, DB_FIELDS)
|
|
}, [settings])
|
|
|
|
const poolingConfigurationShared = supavisorConfig?.find((x) => x.database_type === 'PRIMARY')
|
|
const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined
|
|
|
|
const connectionStringsShared = useMemo(
|
|
() =>
|
|
getConnectionStrings({
|
|
connectionInfo,
|
|
poolingInfo: {
|
|
connectionString: poolingConfigurationShared?.connection_string ?? '',
|
|
db_host: poolingConfigurationShared?.db_host ?? '',
|
|
db_name: poolingConfigurationShared?.db_name ?? '',
|
|
db_port: poolingConfigurationShared?.db_port ?? 0,
|
|
db_user: poolingConfigurationShared?.db_user ?? '',
|
|
},
|
|
metadata: { projectRef },
|
|
}),
|
|
[connectionInfo, poolingConfigurationShared, projectRef]
|
|
)
|
|
|
|
const connectionStringsDedicated = useMemo(
|
|
() =>
|
|
poolingConfigurationDedicated !== undefined
|
|
? getConnectionStrings({
|
|
connectionInfo,
|
|
poolingInfo: {
|
|
connectionString: poolingConfigurationDedicated.connection_string,
|
|
db_host: poolingConfigurationDedicated.db_host,
|
|
db_name: poolingConfigurationDedicated.db_name,
|
|
db_port: poolingConfigurationDedicated.db_port,
|
|
db_user: poolingConfigurationDedicated.db_user,
|
|
},
|
|
metadata: { projectRef },
|
|
})
|
|
: undefined,
|
|
[connectionInfo, poolingConfigurationDedicated, projectRef]
|
|
)
|
|
|
|
return useMemo(
|
|
() =>
|
|
buildConnectionStringPooler({
|
|
deploymentMode,
|
|
connectionInfo,
|
|
connectionStringsShared,
|
|
connectionStringsDedicated,
|
|
ipv4Addon: !!ipv4Addon,
|
|
isHighAvailability,
|
|
}),
|
|
[
|
|
deploymentMode,
|
|
connectionInfo,
|
|
connectionStringsShared,
|
|
connectionStringsDedicated,
|
|
ipv4Addon,
|
|
isHighAvailability,
|
|
]
|
|
)
|
|
}
|
|
|
|
// Vite needs `import.meta.glob` to statically discover the step content
|
|
// modules because the `${filePath}` template can span multiple directory
|
|
// segments (`flask/supabasepy`, `steps/shadcn/command`, ...) which Vite's
|
|
// dynamic-import-vars plugin can't analyze. Skip the glob on the SSR bundle
|
|
// — Vite replaces `import.meta.env.SSR` at build time and tree-shakes the
|
|
// call so the 37 content modules stay out of the server graph (pulling them
|
|
// in reshuffles chunks enough to surface latent circular-dep bugs in
|
|
// unrelated modules). Next/webpack doesn't know about `import.meta.glob`
|
|
// either; the try/catch lets that branch fall through to the webpack-friendly
|
|
// `import()` below.
|
|
let contentModules: Record<string, () => Promise<unknown>> = {}
|
|
if (!import.meta.env?.SSR) {
|
|
try {
|
|
contentModules = import.meta.glob('./content/**/content.{tsx,ts}')
|
|
} catch {
|
|
// webpack build: import.meta.glob is undefined, keep empty map
|
|
}
|
|
}
|
|
|
|
type StepContentModule = { default: ComponentType<StepContentProps> }
|
|
|
|
/**
|
|
* Dynamically loads and renders a content component from the content directory.
|
|
* All step content uses this unified loader - no built-in component registry needed.
|
|
*/
|
|
function StepContent({
|
|
contentId,
|
|
state,
|
|
projectKeys,
|
|
connectionStringPooler,
|
|
deploymentMode,
|
|
}: {
|
|
contentId: string
|
|
state: ConnectState
|
|
projectKeys: ProjectKeys
|
|
connectionStringPooler: ConnectionStringPooler
|
|
deploymentMode: DeploymentMode
|
|
}) {
|
|
// Resolve any template placeholders in the content path
|
|
const filePath = useMemo(() => resolveContentPath(contentId, state), [contentId, state])
|
|
|
|
// Dynamically import the content component
|
|
const ContentComponent = useMemo(() => {
|
|
const viteLoader =
|
|
contentModules[`./content/${filePath}/content.tsx`] ??
|
|
contentModules[`./content/${filePath}/content.ts`]
|
|
|
|
const loader = viteLoader
|
|
? (viteLoader as () => Promise<StepContentModule>)
|
|
: () =>
|
|
import(/* @vite-ignore */ `./content/${filePath}/content`) as Promise<StepContentModule>
|
|
|
|
return dynamic<StepContentProps>(loader, {
|
|
loading: () => (
|
|
<div className="p-4 min-h-[200px]">
|
|
<GenericSkeletonLoader />
|
|
</div>
|
|
),
|
|
})
|
|
}, [filePath])
|
|
|
|
return (
|
|
<ContentComponent
|
|
state={state}
|
|
projectKeys={projectKeys}
|
|
connectionStringPooler={connectionStringPooler}
|
|
deploymentMode={deploymentMode}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsSectionProps) {
|
|
const { ref } = useParams()
|
|
const stepsContainerRef = useRef<HTMLDivElement | null>(null)
|
|
const deploymentMode = useDeploymentMode()
|
|
const connectionStringPooler = useConnectionStringPooler(deploymentMode)
|
|
|
|
const { data: ipv4Addon } = useProjectAddonsQuery(
|
|
{ projectRef: ref },
|
|
{
|
|
select: (data) => {
|
|
const selectedAddons = data?.selected_addons ?? []
|
|
return selectedAddons.find((addon) => addon.type === 'ipv4')
|
|
},
|
|
}
|
|
)
|
|
const showIpv4AddonNotice = shouldShowIpv4AddonNotice({
|
|
isPlatform: deploymentMode.isPlatform,
|
|
mode: state.mode,
|
|
connectionMethod: state.connectionMethod,
|
|
useSharedPooler: state.useSharedPooler,
|
|
hasIpv4Addon: !!ipv4Addon,
|
|
})
|
|
const showSessionPoolerNotice = shouldShowSessionPoolerNotice({
|
|
isPlatform: deploymentMode.isPlatform,
|
|
mode: state.mode,
|
|
connectionMethod: state.connectionMethod,
|
|
})
|
|
const showSelfHostedMcpNotice = shouldShowSelfHostedMcpNotice({
|
|
isSelfHosted: deploymentMode.isSelfHosted,
|
|
mode: state.mode,
|
|
})
|
|
|
|
const shouldFetchDataApiStatus = shouldFetchDataApiConfig({
|
|
mode: state.mode,
|
|
})
|
|
const {
|
|
isEnabled: isDataApiEnabled,
|
|
isPending: isDataApiConfigPending,
|
|
isError: isDataApiConfigError,
|
|
} = useIsDataApiEnabled({
|
|
projectRef: ref,
|
|
enabled: shouldFetchDataApiStatus,
|
|
})
|
|
const showDataApiDisabledWarning = shouldShowDataApiDisabledWarning({
|
|
mode: state.mode,
|
|
isDataApiEnabled,
|
|
isPending: isDataApiConfigPending,
|
|
isError: isDataApiConfigError,
|
|
})
|
|
if (steps.length === 0) return null
|
|
|
|
return (
|
|
<div className="bg-muted/50 flex-1">
|
|
<div className="p-8 flex flex-col gap-y-6">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<h3>Follow these steps</h3>
|
|
<CopyPromptButton stepsContainerRef={stepsContainerRef} />
|
|
</div>
|
|
|
|
{showDataApiDisabledWarning && (
|
|
<Admonition
|
|
type="warning"
|
|
layout="responsive"
|
|
title="Database access requires the Data API"
|
|
description="Client library database queries will not work until the Data API is enabled."
|
|
actions={[
|
|
<Button asChild key="enable" variant="default">
|
|
<Link href={`/project/${ref}/integrations/data_api`}>Enable Data API</Link>
|
|
</Button>,
|
|
]}
|
|
/>
|
|
)}
|
|
|
|
{showIpv4AddonNotice && (
|
|
<Admonition
|
|
type="default"
|
|
layout="responsive"
|
|
title={`${state.connectionMethod === 'direct' ? 'Direct connections use' : 'Transaction pooler uses'} IPv6 by default`}
|
|
description={
|
|
<>
|
|
Enable the dedicated IPv4 address add-on to connect from IPv4-only networks.{' '}
|
|
<InlineLink href={`${DOCS_URL}/guides/platform/ipv4-address`}>
|
|
Learn more
|
|
</InlineLink>
|
|
</>
|
|
}
|
|
actions={
|
|
<Button asChild variant="default">
|
|
<Link href={`/project/${ref}/settings/addons?panel=ipv4`}>Enable IPv4 add-on</Link>
|
|
</Button>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{showSessionPoolerNotice && (
|
|
<Admonition
|
|
type="default"
|
|
title="Only use session pooler on an IPv4 network"
|
|
description="Session pooler connections are IPv4 proxied for free. Use direct connection if connecting via an IPv6 network."
|
|
/>
|
|
)}
|
|
|
|
{showSelfHostedMcpNotice && (
|
|
<Admonition
|
|
type="default"
|
|
title="MCP for self-hosted Supabase requires extra setup"
|
|
description="The configuration below points at the hosted Supabase MCP server. To use MCP against your self-hosted instance, follow the self-hosted MCP guide."
|
|
actions={[
|
|
<DocsButton key="docs" href={`${DOCS_URL}/guides/self-hosting/enable-mcp`} />,
|
|
]}
|
|
/>
|
|
)}
|
|
|
|
<div ref={stepsContainerRef}>
|
|
{steps.map((step, index) => (
|
|
<ConnectSheetStep
|
|
key={step.id}
|
|
number={index + 1}
|
|
title={step.title}
|
|
description={step.description}
|
|
optional={step.optional}
|
|
>
|
|
<StepContent
|
|
contentId={step.content}
|
|
state={state}
|
|
projectKeys={projectKeys}
|
|
connectionStringPooler={connectionStringPooler}
|
|
deploymentMode={deploymentMode}
|
|
/>
|
|
</ConnectSheetStep>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|