Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 108 additions & 12 deletions apps/docs/content/guides/database/connecting-to-postgres.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -186,24 +186,120 @@ This error occurs when your credentials are incorrect. Double-check your usernam

Supabase’s default direct connection supports IPv6 only. To connect over IPv4, consider using the Supavisor session or transaction modes, or a connection pooler (shared or dedicated), which support both IPv4 and IPv6.

### How do you choose a connection method?
### Where is the Postgres connection string in Supabase?

- Direct connection: Persistent backend services (IPv6 only)
- Supavisor session mode: Persistent backend needing IPv4
- Supavisor transaction mode: Serverless functions
- Shared pooler: General-purpose connections with IPv4 and IPv6
- Dedicated pooler: High-performance apps requiring dedicated resources (paid tier)
Your connection string is located in the Supabase Dashboard. Click the [Connect](/dashboard/project/_?showConnect=true) button at the top of the page.

### Where is the Postgres connection string in Supabase?
### Can you use Supavisor and PgBouncer together?

You can technically use both, but it’s not recommended unless you’re specifically trying to increase the total number of concurrent client connections. In most cases, it is better to choose either PgBouncer or Supavisor for pooled or transaction-based traffic. Direct connections remain the best choice for long-lived sessions, and, if IPv4 is required for those sessions, Supavisor session mode can be used as an alternative. Running both poolers simultaneously increases the risk of hitting your database’s maximum connection limit on smaller compute tiers.

### How does the default pool size work?

Supavisor and PgBouncer work independently, but both reference the same pool size setting. For example, if you set the pool size to 30, Supavisor can open up to 30 server-side connections to Postgres each for its session mode port (5432) and transaction mode port (6543), and PgBouncer can also open up to 30. If both poolers are active and reach their roles/modes limits at the same time, you could have as many as 60 backend connections hitting your database, in addition to any direct connections. You can adjust the pool size in [Database settings](/dashboard/project/_/database/settings) in the dashboard.

### What is the difference between client connections and backend connections?

There are two different limits to understand when working with poolers. The first is client connections, which refers to how many clients can connect to a pooler at the same time. This number is capped by your [compute tier’s “max pooler clients” limit](/docs/guides/platform/compute-and-disk#postgres-replication-slots-wal-senders-and-connections), and it applies independently to Supavisor and PgBouncer. The second is backend connections, which is the number of active connections a pooler opens to Postgres. This number is set by the pool size for that pooler.

```
Total backend load on Postgres =
Direct connections +
Supavisor backend connections (≤ supavisor_pool_size) +
PgBouncer backend connections (≤ pgbouncer_pool_size)
≤ Postgres max connections for your compute instance
```

### What is the max pooler clients limit?

The “max pooler clients” limit for your compute tier applies separately to Supavisor and PgBouncer. One pooler reaching its client limit does not affect the other. When a pooler reaches this limit, it stops accepting new client connections until existing ones are closed, but the other pooler remains unaffected. You can check your tier’s connection limits in the [compute and disk limits documentation](/docs/guides/platform/compute-and-disk#postgres-replication-slots-wal-senders-and-connections).

Your connection string is located in the Supabase Dashboard. Click the "Connect" button at the top of the page.
### Where can you see current connection usage?

### Can you use `psql` with a Supabase database?
You can track connection usage from the [Reports](/dashboard/project/_/reports/database) section in your project dashboard. There are three key reports:

Yes. Use the following command structure, replacing `your_connection_string` with the string from your Supabase dashboard:
- **Database Connections:** shows total active connections by role (this includes direct and pooled connections).
- **Dedicated Pooler Client Connections:** shows the number of active client connections to PgBouncer.
- **Shared Pooler (Supavisor) Client Connections:** shows the number of active client connections to Supavisor.

Keep in mind that the Roles page is not real-time, it shows the connection count from the last refresh. If you need up-to-the-second data, set up Grafana or run the query against `pg_stat_activity` directly in SQL Editor. We have a few helpful queries for checking connections.

```sql
-- Count connections by application and user name
select
count(usename),
count(application_name),
application_name,
usename
from
pg_stat_ssl
join pg_stat_activity on pg_stat_ssl.pid = pg_stat_activity.pid
group by usename, application_name;
```
psql "your_connection_string"

```sql
-- View all connections
SELECT
pg_stat_activity.pid,
ssl AS ssl_connection,
datname AS database,
usename AS connected_role,
application_name,
client_addr,
query,
query_start,
state,
backend_start
FROM pg_stat_ssl
JOIN pg_stat_activity
ON pg_stat_ssl.pid = pg_stat_activity.pid;
```

Ensure you have `psql` installed locally before running this command.
### Why are there active connections when the app is idle?

Even if your application isn’t making queries, some Supabase services keep persistent connections to your database. For example, Storage, PostgREST, and our health checker all maintain long-lived connections. You usually see a small baseline of active connections from these services.

### Why do connection strings have different ports?

Different modes use different ports:

- Direct connection: `5432` (database server)
- PgBouncer: `6543` (database server)
- Supavisor transaction mode: `6543` (separate server)
- Supavisor session mode: `5432` (separate server)

The port helps route the connection to the right pooler/mode.

### Does connection pooling affect latency?

Because the dedicated pooler is hosted on the same machine as your database, it connects with lower latency than the shared pooler, which is hosted on a separate server. Direct connections have no pooler overhead but require IPv6 unless you have the IPv4 add-on.

### How to choose the right connection method?

**Direct connection:**

- Best for: persistent backend services
- Limitation: IPv6 only

**Shared pooler:**

- Best for: general-purpose connections (supports IPv4 and IPv6)
- Supavisor session mode → persistent backend that require IPv4
- Supavisor transaction mode → serverless functions or short-lived tasks

**Dedicated pooler (paid tier):**

- Best for: high-performance apps that need dedicated resources
- Uses PgBouncer

You can follow the decision flow in the connection method diagram to quickly choose the right option for your environment.

<Image
alt="Decision tree diagram showing when to connect directly to Postgres or use a connection pooler."
src={{
dark: '/docs/img/guides/database/connecting-to-postgres/connection-decision-tree.svg',
light: '/docs/img/guides/database/connecting-to-postgres/connection-decision-tree-light.svg',
}}
caption="Choosing between direct Postgres connections and connection pooling"
zoomable
/>
1 change: 1 addition & 0 deletions apps/docs/public/humans.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Ignacio Dobronich
Illia Basalaiev
Inian P
Ivan Vasilov
Jeff Smick
Jenny Kibiri
Jess Shears
Jim Chanco Jr
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 7 additions & 22 deletions apps/studio/components/interfaces/App/RouteValidationWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,22 @@ import { toast } from 'sonner'
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsMFAEnabled, useParams } from 'common'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useDashboardHistory } from 'hooks/misc/useDashboardHistory'
import useLatest from 'hooks/misc/useLatest'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'

// Ideally these could all be within a _middleware when we use Next 12
const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
const router = useRouter()
const { ref, slug, id } = useParams()
const { data: organization } = useSelectedOrganizationQuery()

const isLoggedIn = useIsLoggedIn()
const snap = useAppStateSnapshot()
const isUserMFAEnabled = useIsMFAEnabled()

const { data: organization } = useSelectedOrganizationQuery()

const [dashboardHistory, _, { isSuccess: isSuccessStorage }] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.DASHBOARD_HISTORY(ref ?? ''),
{ editor: undefined, sql: undefined }
)

const { setLastVisitedSnippet, setLastVisitedTable } = useDashboardHistory()
const [__, setLastVisitedOrganization] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
''
Expand Down Expand Up @@ -106,22 +100,13 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
useEffect(() => {
if (ref !== undefined && id !== undefined) {
if (router.pathname.endsWith('/sql/[id]') && id !== 'new') {
snap.setDashboardHistory(ref, 'sql', id)
}
if (router.pathname.endsWith('/editor/[id]')) {
snap.setDashboardHistory(ref, 'editor', id)
setLastVisitedSnippet(id)
} else if (router.pathname.endsWith('/editor/[id]')) {
setLastVisitedTable(id)
}
}
}, [ref, id])

useEffect(() => {
// Load dashboard history into app state
if (isSuccessStorage && ref) {
snap.setDashboardHistory(ref, 'editor', dashboardHistory.editor)
snap.setDashboardHistory(ref, 'sql', dashboardHistory.sql)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccessStorage, ref])
}, [ref, id])

useEffect(() => {
if (organization) {
Expand Down
19 changes: 16 additions & 3 deletions apps/studio/components/interfaces/Connect/Connect.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { ExternalLink, Plug } from 'lucide-react'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
import { useMemo, useState } from 'react'

import { DatabaseConnectionString } from 'components/interfaces/Connect/DatabaseConnectionString'
Expand Down Expand Up @@ -55,6 +55,8 @@ export const Connect = () => {
parseAsBoolean.withDefault(false)
)

const [tab, setTab] = useQueryState('tab', parseAsString.withDefault('direct'))

const [connectionObject, setConnectionObject] = useState<ConnectionType[]>(frameworks)
const [selectedParent, setSelectedParent] = useState(connectionObject[0].key) // aka nextjs
const [selectedChild, setSelectedChild] = useState(
Expand Down Expand Up @@ -122,6 +124,8 @@ export const Connect = () => {
}

function handleConnectionType(type: string) {
setTab(type)

if (type === 'frameworks') {
setConnectionObject(frameworks)
handleConnectionTypeChange(frameworks)
Expand Down Expand Up @@ -185,6 +189,15 @@ export const Connect = () => {
selectedGrandchild,
})

const handleDialogChange = (open: boolean) => {
if (!open) {
setShowConnect(null)
setTab(null)
} else {
setShowConnect(open)
}
}

if (!isActiveHealthy) {
return (
<ButtonTooltip
Expand All @@ -205,7 +218,7 @@ export const Connect = () => {
}

return (
<Dialog open={showConnect} onOpenChange={(open) => setShowConnect(!open ? null : open)}>
<Dialog open={showConnect} onOpenChange={handleDialogChange}>
<DialogTrigger asChild>
<Button type="default" className="rounded-full" icon={<Plug className="rotate-90" />}>
<span>Connect</span>
Expand All @@ -219,7 +232,7 @@ export const Connect = () => {
</DialogDescription>
</DialogHeader>

<Tabs_Shadcn_ defaultValue="direct" onValueChange={(value) => handleConnectionType(value)}>
<Tabs_Shadcn_ defaultValue={tab} onValueChange={(value) => handleConnectionType(value)}>
<TabsList_Shadcn_ className={cn('flex overflow-x-scroll gap-x-4', DIALOG_PADDING_X)}>
{connectionTypes.map((type) => (
<TabsTrigger_Shadcn_ key={type.key} value={type.key} className="px-0">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import {
isView,
} from 'data/table-editor/table-editor-types'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useDashboardHistory } from 'hooks/misc/useDashboardHistory'
import { useUrlState } from 'hooks/ui/useUrlState'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useAppStateSnapshot } from 'state/app-state'
import { TableEditorTableStateContextProvider } from 'state/table-editor-table'
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
import { Button } from 'ui'
Expand All @@ -34,8 +34,8 @@ export const TableGridEditor = ({
selectedTable,
}: TableGridEditorProps) => {
const router = useRouter()
const appSnap = useAppStateSnapshot()
const { ref: projectRef, id } = useParams()
const { setLastVisitedTable } = useDashboardHistory()

const tabs = useTabsStateSnapshot()

Expand All @@ -58,10 +58,6 @@ export const TableGridEditor = ({
const tabId = !!id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined
const openTabs = tabs.openTabs.filter((x) => !x.startsWith('sql'))

const onClearDashboardHistory = useCallback(() => {
if (projectRef) appSnap.setDashboardHistory(projectRef, 'editor', undefined)
}, [appSnap, projectRef])

const onTableCreated = useCallback(
(table: { id: number }) => {
router.push(`/project/${projectRef}/editor/${table.id}`)
Expand All @@ -74,9 +70,14 @@ export const TableGridEditor = ({
if (selectedTable) {
// Close tab
const tabId = createTabId(selectedTable.entity_type, { id: selectedTable.id })
tabs.handleTabClose({ id: tabId, router, editor: 'table', onClearDashboardHistory })
tabs.handleTabClose({
id: tabId,
router,
editor: 'table',
onClearDashboardHistory: () => setLastVisitedTable(undefined),
})
}
}, [onClearDashboardHistory, router, selectedTable, tabs])
}, [router, selectedTable, tabs])

const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })

Expand Down Expand Up @@ -111,7 +112,7 @@ export const TableGridEditor = ({
id: tabId,
router,
editor: 'table',
onClearDashboardHistory,
onClearDashboardHistory: () => setLastVisitedTable(undefined),
})
}}
>
Expand All @@ -122,7 +123,7 @@ export const TableGridEditor = ({
asChild
type="default"
className="mt-2"
onClick={() => appSnap.setDashboardHistory(projectRef, 'editor', undefined)}
onClick={() => setLastVisitedTable(undefined)}
>
<Link href={`/project/${projectRef}/editor/${openTabs[0].split('-')[1]}`}>
Close tab
Expand All @@ -133,7 +134,7 @@ export const TableGridEditor = ({
asChild
type="default"
className="mt-2"
onClick={() => appSnap.setDashboardHistory(projectRef, 'editor', undefined)}
onClick={() => setLastVisitedTable(undefined)}
>
<Link href={`/project/${projectRef}/editor`}>Head back</Link>
</Button>
Expand Down
10 changes: 6 additions & 4 deletions apps/studio/components/layouts/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Plus, X } from 'lucide-react'
import { useRouter } from 'next/router'

import { useParams } from 'common'
import { useAppStateSnapshot } from 'state/app-state'
import { useDashboardHistory } from 'hooks/misc/useDashboardHistory'
import { editorEntityTypes, useTabsStateSnapshot, type Tab } from 'state/tabs'
import {
cn,
Expand All @@ -32,7 +32,7 @@ import { TabPreview } from './TabPreview'
export const EditorTabs = () => {
const { ref, id } = useParams()
const router = useRouter()
const appSnap = useAppStateSnapshot()
const { setLastVisitedSnippet, setLastVisitedTable } = useDashboardHistory()

const editor = useEditorType()
const tabs = useTabsStateSnapshot()
Expand Down Expand Up @@ -68,8 +68,10 @@ export const EditorTabs = () => {
}

const onClearDashboardHistory = () => {
if (ref && editor) {
appSnap.setDashboardHistory(ref, editor === 'table' ? 'editor' : editor, undefined)
if (editor === 'table') {
setLastVisitedTable(undefined)
} else if (editor === 'sql') {
setLastVisitedSnippet(undefined)
}
}

Expand Down
30 changes: 30 additions & 0 deletions apps/studio/hooks/misc/useDashboardHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useLocalStorageQuery } from './useLocalStorage'

type DashboardHistory = { editor?: string; sql?: string }
const DEFAULT_HISTORY = { editor: undefined, sql: undefined }

export const useDashboardHistory = () => {
// [Joshen] History should always refer to the project that the user is currently on
const { ref } = useParams()

const [history, setHistory, { isSuccess }] = useLocalStorageQuery<DashboardHistory>(
LOCAL_STORAGE_KEYS.DASHBOARD_HISTORY(ref ?? ''),
DEFAULT_HISTORY
)

const setLastVisitedTable = (id?: string) => {
setHistory({ ...history, editor: id })
}

const setLastVisitedSnippet = (id?: string) => {
setHistory({ ...history, sql: id })
}

return {
history,
setLastVisitedTable,
setLastVisitedSnippet,
isHistoryLoaded: isSuccess,
}
}
Loading
Loading