Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -3092,6 +3092,7 @@ export const self_hosting: NavMenuConstant = {
items: [
{ name: 'Overview', url: '/guides/self-hosting' },
{ name: 'Deploy with Docker', url: '/guides/self-hosting/docker' },
{ name: 'Accessing Postgres', url: '/guides/self-hosting/accessing-postgres' },
{ name: 'Configure new API keys', url: '/guides/self-hosting/self-hosted-auth-keys' },
{ name: 'Learn about API Gateway', url: '/guides/self-hosting/self-hosted-envoy' },
{
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/guides/api/rest/generating-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ or in case of local development:
npx supabase gen types typescript --local > database.types.ts
```

or in case of a self-hosted instance (see [Accessing Postgres](/docs/guides/self-hosting/docker#accessing-postgres) for more information):
or in case of a self-hosted instance (see [Accessing Postgres](/docs/guides/self-hosting/accessing-postgres#connect-through-supavisor) for more information):

```bash
npx supabase gen types typescript --db-url postgres://postgres.[POOLER_TENANT_ID]:[POSTGRES_PASSWORD]@[your-domain-or-ip]:5432/postgres --schema public > database.types.ts
Expand Down
128 changes: 128 additions & 0 deletions apps/docs/content/guides/self-hosting/accessing-postgres.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
title: 'Accessing Postgres'
description: 'Connect to your self-hosted Postgres database through the Supavisor or PgBouncer pooler, or with a direct connection.'
subtitle: 'Connect to your self-hosted Postgres database through the Supavisor or PgBouncer pooler, or with a direct connection.'
---

This guide explains how to connect to Postgres in self-hosted Supabase, using the Supavisor pooler, the optional PgBouncer pooler, or a direct connection.

Self-hosted Supabase uses [Supavisor](https://github.com/supabase/supavisor) as its default connection pooler. A pooler sits in front of Postgres and shares a small set of database connections across many clients, which avoids exhausting Postgres connection limits.

## Choose a connection mode

Self-hosted Supabase offers three ways to reach Postgres:

- **Session mode** - Supavisor on port `5432`. Best for persistent clients that need per-session features such as `SET` statements, prepared statements, `LISTEN/NOTIFY`, or advisory locks. Each client holds a dedicated Postgres connection for the life of the session. Available by default.
- **Transaction mode** - Supavisor or PgBouncer on port `6543`. Best for serverless or edge functions that open many short-lived connections. Does not support session-level features (`SET`, `LISTEN/NOTIFY`, temporary tables that span transactions, or advisory locks). Supavisor pooler does not support prepared statements; PgBouncer can be [configured to support them](#use-pgbouncer-instead-of-supavisor). Available by default.
- **Direct connection** - Postgres bypassing the pooler. Not exposed by default - refer to [exposing Postgres](#expose-postgres-for-direct-connections). Best for migrations, `pg_dump`, and long-lived backends.

## Connect through Supavisor [#connect-through-supavisor]

Use your domain name, your server IP, or `localhost`, depending on where the stack runs.

For session-mode connections:

```sh
psql 'postgres://postgres.[POOLER_TENANT_ID]:[POSTGRES_PASSWORD]@[your-domain]:5432/postgres'
```

For transaction-mode connections:

```sh
psql 'postgres://postgres.[POOLER_TENANT_ID]:[POSTGRES_PASSWORD]@[your-domain]:6543/postgres'
```

Supavisor requires the "tenant ID" (`your-tenant-id`) for authentication, not only the role. When using `psql` with command-line parameters instead of a connection string, the `-U` parameter must also be `postgres.[POOLER_TENANT_ID]`.

## Customize Supavisor

Configure Supavisor settings through your `.env` file, then recreate the stack for changes to take effect:

| Variable | Default | Description |
| ------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `POSTGRES_PORT` | `5432` | Host port for session-mode connections. |
| `POOLER_PROXY_PORT_TRANSACTION` | `6543` | Host port for transaction-mode connections. |
| `POOLER_DEFAULT_POOL_SIZE` | `20` | Postgres connections the pooler opens per pool. Keep this below your Postgres `max_connections` minus connections reserved for other services. |
| `POOLER_MAX_CLIENT_CONN` | `100` | Client connections the pooler accepts. |
| `POOLER_TENANT_ID` | `your-tenant-id` | Supavisor tenant identifier, used in the username. |
| `POOLER_DB_POOL_SIZE` | `5` | Internal metadata pool used by Supavisor itself. |

To check your current Postgres `max_connections` setting:

```sh
docker compose exec db psql -U postgres -c "SHOW max_connections;"
```

To change `max_connections` or other Postgres settings, refer to [custom Postgres configuration](/docs/guides/self-hosting/postgres-upgrade-17#custom-postgres-configuration).

For the full list of Supavisor's configurable environment variables, check the reference list in [docker/CONFIG.md](https://github.com/supabase/supabase/blob/master/docker/CONFIG.md#supavisor).

## Use PgBouncer instead of Supavisor

Self-hosted Supabase includes an optional [PgBouncer](https://www.pgbouncer.org/) override. It disables Supavisor and runs PgBouncer in transaction mode on `POOLER_PROXY_PORT_TRANSACTION`.

Add it to your stack with `run.sh`:

```sh
sh run.sh config add pgbouncer
sh run.sh start
```

If you prefer to run Docker Compose commands explicitly, use `docker compose -f docker-compose.yml -f docker-compose.pgbouncer.yml up -d`.

To connect as `postgres`:

```sh
# tenant ID isn't required for PgBouncer
psql 'postgres://postgres:[POSTGRES_PASSWORD]@[your-domain]:6543/postgres'
```

The PgBouncer override provides transaction mode only. For session-mode connections, or for features that transaction mode does not support (such as `SET` statements or `LISTEN/NOTIFY`), reconfigure PgBouncer manually by editing its environment variables in `docker-compose.pgbouncer.yml`, or use a [direct connection](#expose-postgres-for-direct-connections). PgBouncer reuses the `POOLER_DEFAULT_POOL_SIZE` and `POOLER_MAX_CLIENT_CONN` values from your `.env` configuration.

## Expose Postgres for direct connections

In the default configuration, Postgres is only reachable through the pooler. To bypass the pooler for migrations, `pg_dump`, or other direct-connection needs, expose the Postgres port.

<Admonition type="danger">

Exposing Postgres opens your database to the network. Configure firewall rules or network policies to restrict access to Postgres.

</Admonition>

If you use the default Supavisor stack, edit `docker-compose.yml`:

1. Disable Supavisor by commenting out or removing the entire `supavisor` service section.
2. Expose the Postgres port by adding the port mapping to the `db` service:

```yaml name=docker-compose.yml
db:
ports:
- ${POSTGRES_PORT}:${POSTGRES_PORT}
container_name: supabase-db
```

<Admonition type="note">

If you want to keep Supavisor running alongside a direct connection, map Postgres to a different host port (for example, `5433:${POSTGRES_PORT}`) instead of disabling Supavisor.

</Admonition>

If you use the PgBouncer override, Supavisor is already disabled. Uncomment the `db` block in `docker-compose.pgbouncer.yml` instead:

```yaml name=docker-compose.pgbouncer.yml
db:
ports:
- ${POSTGRES_PORT}:${POSTGRES_PORT}
```

After restarting, connect directly with a standard Postgres connection string:

```sh
postgres://postgres:[POSTGRES_PASSWORD]@[your-server-ip]:5432/[POSTGRES_DB]
```

## Additional resources

- [Supavisor documentation](https://supabase.github.io/supavisor/development/docs/)
- [PgBouncer documentation](https://www.pgbouncer.org/config.html)
- [Connect to your database](/docs/guides/database/connecting-to-postgres)
59 changes: 1 addition & 58 deletions apps/docs/content/guides/self-hosting/docker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -319,27 +319,7 @@ You will be prompted for a username and password. See the [Studio authentication

## Accessing Postgres

The self-hosted Supabase stack provides the [Supavisor](https://supabase.github.io/supavisor/development/docs/) connection pooler for accessing Postgres and managing database connections.

You can connect to the Postgres database via Supavisor using the methods described below. Use your domain name, your server IP, or `localhost` depending on whether you are running self-hosted Supabase on a VPS, or locally.

The default `POOLER_TENANT_ID` is `your-tenant-id` (can be later changed in `.env`), and the password is the value of `POSTGRES_PASSWORD` from the `.env` file.

For session-mode connections (equivalent to a direct Postgres connection):

```sh
psql 'postgres://postgres.[POOLER_TENANT_ID]:[POSTGRES_PASSWORD]@[your-domain]:5432/postgres'
```

For transaction-mode connections:

```sh
psql 'postgres://postgres.[POOLER_TENANT_ID]:[POSTGRES_PASSWORD]@[your-domain]:6543/postgres'
```

When using `psql` with command-line parameters instead of a connection string to connect to Supavisor, the `-U` parameter should also be `postgres.[POOLER_TENANT_ID]`.

If you need to configure Postgres to be directly accessible from the Internet, read [Exposing your Postgres database](#exposing-your-postgres-database).
Self-hosted Supabase pools Postgres connections through Supavisor by default, with an optional PgBouncer pooler and a direct-connection option. For connection strings, pooler configuration, switching poolers, and exposing Postgres directly, see [Accessing Postgres](/docs/guides/self-hosting/accessing-postgres).

To change the database password, read [Changing database password](#changing-database-password).

Expand Down Expand Up @@ -590,43 +570,6 @@ By default, the Storage backend uses local files via a bind mount. On macOS, Doc

Configuring the Supabase AI Assistant is optional. By adding **your own** `OPENAI_API_KEY` to `.env` you can enable AI services, which help with writing SQL queries, statements, and policies.

### Accessing Postgres through Supavisor

By default, Postgres connections go through the Supavisor connection pooler for efficient connection management. Two ports are available:

- `POSTGRES_PORT` (default: 5432) - Session mode, behaves like a direct Postgres connection
- `POOLER_PROXY_PORT_TRANSACTION` (default: 6543) - Transaction mode, uses connection pooling

For more information on configuring and using Supavisor, see the [Supavisor documentation](https://supabase.github.io/supavisor/).

### Exposing your Postgres database

By default, Postgres is only accessible through Supavisor. If you need direct access to the database (bypassing the connection pooler), you need to disable Supavisor and expose the Postgres port.

<Admonition type="danger">

Exposing Postgres directly bypasses connection pooling and exposes your database to the network. Configure firewall rules or network policies to restrict access to trusted IPs only.

</Admonition>

Edit `docker-compose.yml`:

1. **Disable Supavisor** - Comment out or remove the entire `supavisor` service section
2. **Expose Postgres port** - Add the port mapping to the `db` service, it should look like the example below:

```yaml name=docker-compose.yml
db:
ports:
- ${POSTGRES_PORT}:${POSTGRES_PORT}
container_name: supabase-db
```

After restarting, you can connect to the database directly using a standard Postgres connection string:

```sh
postgres://postgres:[POSTGRES_PASSWORD]@[your-server-ip]:5432/[POSTGRES_DB]
```

### Setting log_min_messages in Postgres

By default, the database's `log_min_messages` configuration is set to `fatal` in [docker-compose.yml](https://github.com/supabase/supabase/blob/df8729a82b1847e2989c14ede27965612761d503/docker/docker-compose.yml#L466) to prevent redundant logs generated by Realtime. You can configure `log_min_messages` using any of the Postgres [Severity Levels](https://www.postgresql.org/docs/current/runtime-config-logging.html#RUNTIME-CONFIG-SEVERITY-LEVELS).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Before restoring, check the following on your self-hosted instance:

## Step 4: Restore to your self-hosted database

Connect to your self-hosted Postgres and restore the dump files. The [default](/docs/guides/self-hosting/docker#accessing-postgres) connection string for self-hosted Supabase is:
Connect to your self-hosted Postgres and restore the dump files. The [default](/docs/guides/self-hosting/accessing-postgres#connect-through-supavisor) connection string for self-hosted Supabase is:

```
postgres://postgres.your-tenant-id:[POSTGRES_PASSWORD]@[your-domain]:5432/postgres
Expand Down Expand Up @@ -166,7 +166,7 @@ select * from pg_available_extensions;

### Connection refused

Make sure your self-hosted Postgres port is accessible. In the default [self-hosted Supabase](/docs/guides/self-hosting/docker#accessing-postgres) setup, the user is `postgres.your-tenant-id` with Supavisor on port `5432`.
Make sure your self-hosted Postgres port is accessible. In the default [self-hosted Supabase](/docs/guides/self-hosting/accessing-postgres#connect-through-supavisor) setup, the user is `postgres.your-tenant-id` with Supavisor on port `5432`.

### Legacy Studio configuration

Expand Down
13 changes: 7 additions & 6 deletions apps/docs/features/docs/Troubleshooting.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default async function TroubleshootingPage({ entry }: { entry: ITroublesh
const dateUpdated = entry.data.database_id.startsWith('pseudo-')
? new Date()
: (await getTroubleshootingUpdatedDates()).get(entry.data.database_id)
const errorCodes = [...new Set(entry.data.errors?.map(formatError).filter(Boolean) ?? [])]

return (
<SidebarSkeleton
Expand Down Expand Up @@ -62,25 +63,25 @@ export default async function TroubleshootingPage({ entry }: { entry: ITroublesh
<hr className="my-6" aria-hidden />
</>
)}
{entry.data.errors?.length && entry.data.errors.length > 0 && (
{errorCodes.length > 0 && (
<>
<h3 className="text-sm text-foreground-lighter mb-3">Related error codes</h3>
<div className="flex flex-wrap gap-0.5">
{entry.data.errors.map((error, index) => (
{errorCodes.map((errorCode) => (
<Link
key={index}
href={`/guides/troubleshooting${serializeTroubleshootingSearchParams({ errorCodes: [formatError(error)] })}`}
key={errorCode}
href={`/guides/troubleshooting${serializeTroubleshootingSearchParams({ errorCodes: [errorCode] })}`}
>
<PillTag className="hover:bg-200 focus-visible:bg-foreground-muted hover:border-control focus-visible:border-control transition-colors">
{formatError(error)}
{errorCode}
</PillTag>
</Link>
))}
</div>
<hr className="my-6" aria-hidden />
</>
)}
{entry.data.keywords?.length && entry.data.keywords.length > 0 && (
{!!entry.data.keywords?.length && (
<>
<h3 className="text-sm text-foreground-lighter mb-3">Keywords</h3>
<div className="flex flex-wrap gap-0.5">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,6 @@ describe('MCPToolScopeMappings', () => {
)
})

// Drift guard: the exact tool registry of @supabase/mcp-server-supabase@0.8.1, the version the
// platform pins. When the platform bumps the MCP server, this list (and the mapping) must be
// re-derived from the controller's assertMcpOAuthScope calls.
test('covers exactly the tool registry of the deployed MCP server', () => {
expect(Object.keys(MCPToolScopeMappings).sort()).toEqual([
'apply_migration',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ import {
type TokenAccessEvaluation,
} from '../../AccessToken.roles'
import { useCapabilitySummary } from '../../hooks/useCapabilitySummary'
import { useOrgAndProjectData } from '../../hooks/useOrgAndProjectData'
import { failingResourceLine } from '../ExceedsRoleBadge'
import {
ResourceAccessPills,
OrganizationAccessPill,
ProjectAccessPill,
useResourceAccessWrap,
type ResourceAccessPillItem,
} from '../ResourceAccessPills'
import { CapabilitiesSection } from '../TokenCapabilities/CapabilitiesSection'
import { CapabilityLevelToggle } from '../TokenCapabilities/CapabilityLevelToggle'
Expand All @@ -26,6 +25,7 @@ import {
type CapabilityLevelFilter,
} from '../TokenCapabilities/TokenCapabilities.utils'
import { EXPIRY_OPTIONS, type TokenFormValues } from './NewScopedTokenForm.utils'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import {
getEnabledMcpTools,
PermissionScopeMap,
Expand All @@ -46,7 +46,7 @@ export const NewScopedTokenFormReview = ({
access,
permissionScopeMap,
}: ReviewStepProps) => {
const { organizations, projects } = useOrgAndProjectData()
const { data: organizations = [] } = useOrganizationsQuery()
const selection = values.permissions
const grantedScopes = useMemo(() => selectionToScopes(selection), [selection])

Expand All @@ -70,19 +70,6 @@ export const NewScopedTokenFormReview = ({
[access.effectiveSelection, values.resourceAccess, values.organizationSlugs, values.projectRefs]
)

// The classic (account) flow skips review entirely, so only org- and project-bound tokens land
// here.
const resourceItems = useMemo<ResourceAccessPillItem[]>(() => {
if (values.resourceAccess === 'organization') {
return organizations
.filter((org) => values.organizationSlugs.includes(org.slug))
.map((org) => ({ key: org.slug, label: org.name }))
}
return projects
.filter((project) => values.projectRefs.includes(project.ref))
.map((project) => ({ key: project.ref, label: project.name }))
}, [values, projects, organizations])

const expiresSummary = useMemo(() => {
if (values.expiresAt === 'custom') {
return values.customExpiryDate
Expand Down Expand Up @@ -164,7 +151,20 @@ export const NewScopedTokenFormReview = ({
<dt className="shrink-0 text-sm text-foreground-lighter">Resource access</dt>
<dd className="w-full min-w-0 text-sm text-foreground sm:w-auto sm:flex-1">
<div ref={pillsRef} className="flex flex-wrap justify-start gap-1.5 sm:justify-end">
<ResourceAccessPills resourceAccess={values.resourceAccess} items={resourceItems} />
{values.resourceAccess === 'organization'
? values.organizationSlugs.map((orgSlug) => (
<OrganizationAccessPill
key={orgSlug}
slug={orgSlug}
organization={organizations.find((org) => org.slug === orgSlug)}
/>
))
: null}
{values.resourceAccess === 'project'
? values.projectRefs.map((projectRef) => (
<ProjectAccessPill key={projectRef} projectRef={projectRef} />
))
: null}
</div>
</dd>
</div>
Expand Down
Loading
Loading