mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
For Multigres (HA) projects you can't connect to read replicas directly — reads go through a read-only load balancer on the primary's host at port 5433. Since #44695 stripped the pooler UI, HA projects showed no source option at all in the Connect dialog and still prompted for the IPv4 add-on. This surfaces it as a first-class, clearly-labeled read-only source. In the UI it's labeled `Replica (read-only)` rather than "load balancer" — the primary goes through the same gateway, so "load balancer" would be confusing from a product perspective (internally the `load-balancer` source identifier and `HIGH_AVAILABILITY_LOAD_BALANCER_PORT` constant keep their names). <img width="883" height="342" alt="Screenshot 2026-08-24 at 11 32 26 PM" src="https://github.com/user-attachments/assets/3716f6dd-0325-4b9d-adbc-9ece9244de62" /> **Added:** - Source select for HA projects in the Direct tab: `Primary database` + `Replica (read-only)` (individual replica rows are filtered out — they're only reachable via the load balancer) - Replica (load balancer) connection strings on all 9 connection types: primary host, port `5433`, with the Multigres-required `sslmode=require&sslnegotiation=direct` params (JDBC gets the `sslNegotiation` spelling, .NET gets `SSL Negotiation=Direct`) - `Read-only` badge on the connection code block + note pointing writes at the primary - Programmatic labels for the ConnectSheet select/switch/multi-select fields (the Source combobox previously had no accessible name) **Changed:** - The generated-file step (Node.js/Golang/.NET/Python/SQLAlchemy) is now source-aware — it previously ignored the Source selection entirely (also affected read replicas on normal projects) and silently rendered the primary's connection info - .NET template now emits `Port=` (Npgsql defaults to 5432 when omitted) and the install step actually installs Npgsql (pinned 9.0.5 — `SSL Negotiation` requires 9+) - SQLAlchemy `DATABASE_URL` merges `sslmode=require` into the string's existing query params instead of a hardcoded suffix that could drop TLS - Source option labels normalized to sentence case (`Primary database`, `Read replica (…)`) - `MultipleCodeBlock` (ui-patterns) accepts an optional `className` - HA coercion in `useConnectState` extended: a stale replica `connectionSource` restored from URL/localStorage falls back to the primary **Removed:** - IPv4 add-on admonition for HA projects (the forced-direct method was tripping it; the add-on doesn't apply to Multigres) Out of scope (needs platform work): SQL editor / Data API / other `DatabaseSelector` surfaces — executing against the load balancer requires a platform-issued connection string, and the load-balancers API only returns a REST endpoint today. The `5433` port is a client-side constant (`HIGH_AVAILABILITY_LOAD_BALANCER_PORT`) until the API exposes it. ## To test On an HA (Multigres) project: - Open Connect → Direct: Source shows exactly `Primary database` and `Replica (read-only)`; selecting the replica shows `…@<primary-host>:5433/postgres?sslmode=require&sslnegotiation=direct`, a `Read-only` badge, and the read-only note - Cycle all 9 connection types with the replica selected — every snippet carries port 5433 (`.NET` includes `Port=5433;…;SSL Negotiation=Direct`), badge/note persist - No "Enable IPv4 add-on" admonition anywhere in the Direct tab - Switch tabs / hard-reload: source resets to primary with no stale badge/string combos On a normal project: - Direct tab unchanged: no `Replica (read-only)` option, pooler badges and IPv4 admonitions behave as before, `.NET` now shows `Port=5432` and no `SSL Negotiation` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added read-only load-balancer connection options for high-availability projects. - Added .NET and SQLAlchemy connection examples with required SSL settings. - Added clear read-only labels and notices explaining write restrictions. - **Bug Fixes** - Suppressed IPv4 add-on notices for high-availability connections. - Improved connection-source selection and restored-setting handling. - Improved connection form identification and accessibility. - **Style** - Added customizable styling support for multi-code-block displays. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
138 lines
5.6 KiB
TypeScript
138 lines
5.6 KiB
TypeScript
import { useParams } from 'common'
|
|
import { useMemo } from 'react'
|
|
|
|
import { CONNECTION_SOURCE_LOAD_BALANCER } from './Connect.constants'
|
|
import type { DeploymentMode } from './Connect.types'
|
|
import {
|
|
buildConnectionStringPooler,
|
|
getConnectionStrings,
|
|
getHighAvailabilityLoadBalancerConnectionInfo,
|
|
} from './DatabaseSettings.utils'
|
|
import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
|
|
import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query'
|
|
import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query'
|
|
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
|
|
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
|
|
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
|
import { useIsHighAvailability } from '@/hooks/misc/useSelectedProject'
|
|
import { pluckObjectFields } from '@/lib/helpers'
|
|
|
|
/**
|
|
* [Joshen] ConnectStepsSection does something similar but since only this page needs to consider connection strings
|
|
* from all databases (including read replicas), am opting to separate the logic for retrieving connection strings here
|
|
*
|
|
* We can however, consider to shift this logic into ConnectStepsSection, such that we can consider read replicas for
|
|
* the other tabs like "Framework" and "ORM" too. However, leaving them out for now and only updating "Direct"
|
|
*/
|
|
export const useConnectionStringDatabases = (deploymentMode: DeploymentMode) => {
|
|
const { ref: projectRef } = useParams()
|
|
const { hasAccess: allowPgBouncerSelection } = useCheckEntitlements('dedicated_pooler')
|
|
const isHighAvailability = useIsHighAvailability()
|
|
|
|
const { data: databases = [] } = useReadReplicasQuery({ 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 ?? [])
|
|
|
|
// Memoized so the per-database pooler bag (consumed by resolveConnectionString
|
|
// downstream) keeps a stable identity across renders. Without this the inner
|
|
// pluckObjectFields/getConnectionStrings calls would mint fresh objects every
|
|
// render and ripple through the resolveConnectionString useMemo below.
|
|
return useMemo(() => {
|
|
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
|
|
const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
|
|
|
|
const connectionStringsByIdentifier = Object.fromEntries(
|
|
databases.map((db) => {
|
|
const connectionInfo = pluckObjectFields(db || emptyState, DB_FIELDS)
|
|
const poolingConfigurationShared = supavisorConfig?.find(
|
|
(x) => x.identifier === db.identifier
|
|
)
|
|
const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined
|
|
|
|
const connectionStringsShared = getConnectionStrings({
|
|
connectionInfo,
|
|
poolingInfo: {
|
|
connectionString: poolingConfigurationShared?.connection_string ?? '',
|
|
db_host: poolingConfigurationShared?.db_host ?? '',
|
|
db_name: poolingConfigurationShared?.db_name ?? '',
|
|
db_port: poolingConfigurationShared?.db_port ?? 0,
|
|
db_user: poolingConfigurationShared?.db_user ?? '',
|
|
},
|
|
metadata: { projectRef: db.identifier },
|
|
})
|
|
|
|
const connectionStringsDedicated =
|
|
poolingConfigurationDedicated !== undefined
|
|
? getConnectionStrings({
|
|
connectionInfo,
|
|
poolingInfo: {
|
|
connectionString: poolingConfigurationDedicated.connection_string.replace(
|
|
projectRef ?? '_',
|
|
db.identifier
|
|
),
|
|
db_host: poolingConfigurationDedicated.db_host,
|
|
db_name: poolingConfigurationDedicated.db_name,
|
|
db_port: poolingConfigurationDedicated.db_port,
|
|
db_user: poolingConfigurationDedicated.db_user,
|
|
},
|
|
metadata: { projectRef: db.identifier },
|
|
})
|
|
: undefined
|
|
|
|
return [
|
|
db.identifier,
|
|
buildConnectionStringPooler({
|
|
deploymentMode,
|
|
connectionInfo,
|
|
connectionStringsShared,
|
|
connectionStringsDedicated,
|
|
ipv4Addon: !!ipv4Addon,
|
|
isHighAvailability,
|
|
}),
|
|
]
|
|
})
|
|
)
|
|
|
|
// The Multigres load balancer is not a database row — it shares the
|
|
// primary's host on a dedicated read-only port.
|
|
const primaryDatabase = isHighAvailability
|
|
? databases.find((db) => db.identifier === projectRef)
|
|
: undefined
|
|
if (primaryDatabase) {
|
|
const loadBalancerInfo = getHighAvailabilityLoadBalancerConnectionInfo(
|
|
pluckObjectFields(primaryDatabase, DB_FIELDS)
|
|
)
|
|
connectionStringsByIdentifier[CONNECTION_SOURCE_LOAD_BALANCER] = buildConnectionStringPooler({
|
|
deploymentMode,
|
|
connectionInfo: loadBalancerInfo,
|
|
connectionStringsShared: getConnectionStrings({
|
|
connectionInfo: loadBalancerInfo,
|
|
metadata: { projectRef },
|
|
}),
|
|
ipv4Addon: false,
|
|
isHighAvailability,
|
|
})
|
|
}
|
|
|
|
return connectionStringsByIdentifier
|
|
}, [
|
|
databases,
|
|
pgbouncerConfig,
|
|
supavisorConfig,
|
|
allowPgBouncerSelection,
|
|
ipv4Addon,
|
|
projectRef,
|
|
deploymentMode,
|
|
isHighAvailability,
|
|
])
|
|
}
|