Connecting pgAdmin, DBeaver & TablePlus to Self-Hosted Supabase

Connect pgAdmin, DBeaver, or TablePlus to self-hosted Supabase: the right port, SSH tunnels, SSL setup, and fixes for 'Tenant or user not found' errors.

Cover Image for Connecting pgAdmin, DBeaver & TablePlus to Self-Hosted Supabase

Supabase Studio is fine for browsing tables and running the occasional query. But the moment you need to compare execution plans, edit a function with proper autocomplete, export a filtered result set, or diff two schemas, you reach for a real database client — pgAdmin, DBeaver, TablePlus, DataGrip, whatever your team already uses. One of the genuine perks of running Supabase on your own server is that nothing stops you: it's plain Postgres, and any client can talk to it.

Except the first attempt usually fails. GitHub discussions and Reddit threads are full of the same three errors — Connection refused, FATAL: Tenant or user not found, and SSL handshake failures — and almost all of them come down to picking the wrong port, the wrong username format, or exposing the wrong thing to the internet. This guide walks through the correct setup for the three most popular clients, and how to do it without turning your database into a public endpoint.

Two doors into your database: 5432 vs 6543

A self-hosted Supabase stack exposes Postgres two ways, and GUI clients care about the difference more than most tools do.

Direct connection — port 5432. This is Postgres itself, published by the db container:

Host:     your-server-ip (or db hostname if tunneled)
Port:     5432
Database: postgres
User:     postgres
Password: POSTGRES_PASSWORD from your .env

Supavisor pooler — port 6543 (transaction mode) or 5432 on the pooler (session mode). Pooled connections require the tenant-qualified username:

User: postgres.your-tenant-id   # matches POOLER_TENANT_ID in .env

Forget the .your-tenant-id suffix and Supavisor rejects you with FATAL: Tenant or user not found — even though every container is healthy and the password is correct. It's one of the most-reported self-hosting issues on GitHub Discussions, and it's almost never a real auth failure.

For GUI clients, use the direct connection. Database clients lean heavily on session state: prepared statements, search_path changes, temporary tables, LISTEN/NOTIFY, long-lived transactions in query consoles. Transaction-mode pooling breaks all of that in ways that surface as confusing, intermittent errors. The pooler exists to multiplex hundreds of short-lived app connections — your one interactive session isn't the problem it solves. If you're unsure how Supavisor is wired up in your stack, the connection pooling guide for self-hosted Supabase covers the architecture in detail.

Don't expose 5432 to the internet

Here's the tension: the direct connection is what you want, but publishing port 5432 on a public IP is how self-hosted databases end up in botnet scan logs. Within hours of opening the port you'll see brute-force attempts in your Postgres logs. The fix is not a stronger password — it's not answering strangers at all.

You have three good options, in rough order of preference:

  1. SSH tunnel (recommended, zero extra setup). All three clients in this guide have SSH tunneling built in. The client connects to your server over SSH — which you've already secured — and reaches Postgres on localhost:5432 from the server's perspective. Nothing new is exposed.
  2. A private overlay network. Tailscale in front of your Supabase instance gives every developer a private route to the database with no port forwarding and no tunnel config per client.
  3. Firewall allowlisting. Restrict 5432 to known office/VPN IPs. Workable, but brittle for remote teams on dynamic IPs — see the network security guide for how to do it properly if you go this route.

If port 5432 must stay reachable beyond localhost for other reasons, at minimum enforce TLS on the connection — the database SSL encryption guide walks through certificates and pg_hba.conf rules for self-hosted stacks.

The setups below assume the SSH tunnel approach, since it works everywhere and requires nothing beyond the SSH access you already have.

pgAdmin

pgAdmin has a dedicated SSH tunnel tab, so no manual ssh -L needed.

  1. Register → Server, name it something honest like prod supabase — careful.
  2. Connection tab: Host localhost, Port 5432, Maintenance DB postgres, Username postgres, your POSTGRES_PASSWORD.
  3. SSH Tunnel tab: enable it, set your server's public IP, SSH port, your SSH username, and identity file.

The one pgAdmin quirk worth knowing: with the tunnel enabled, "Host" means the host as seen from the SSH server — hence localhost, not your server's public IP. Putting the public IP in both fields is the most common misconfiguration and yields a timeout, not a helpful error.

Once connected, expand Schemas and you'll see far more than public: auth, storage, realtime, _supavisor, extensions, and friends. Browse freely; edit reluctantly (more below).

DBeaver

  1. New Connection → PostgreSQL.
  2. Main tab: Host localhost, Port 5432, Database postgres, user postgres.
  3. SSH tab: check Use SSH Tunnel, fill in your server details, test the tunnel first — DBeaver tests it separately from the DB connection, which makes diagnosing failures much easier.
  4. On the PostgreSQL tab, check Show all databases. Supabase creates a _supabase database (housing analytics and pooler internals in recent stack versions) that stays invisible otherwise, and people burn time wondering why their client "can't see" what Studio shows.

DBeaver enables SSL negotiation by default (sslmode=prefer), which just works whether or not you've configured server certificates. If you've enforced verify-full on the server, point the SSL tab at your CA certificate.

TablePlus

TablePlus is the least ceremonious of the three:

  1. Create connection → PostgreSQL.
  2. Fill in localhost:5432, database postgres, user postgres.
  3. Toggle Over SSH at the bottom and add the server details.

Tag the connection with a red environment color if it's production. TablePlus commits edits made in the data grid when you press Cmd+S, and the red banner is what stands between you and casually rewriting a production row you only meant to look at.

Which role should you connect as?

The docker-compose stack ships several roles, and it matters which one your client uses:

  • postgres — what you should use. On self-hosted stacks it's superuser-equivalent for practical purposes and, crucially, it bypasses RLS. Every query you run in a GUI client sees all rows in all tables. That's usually what you want for admin work — just be aware your client shows you data your app's users can't see, so a query that "works in DBeaver" may return nothing through PostgREST. The same trap exists for ORMs connecting as privileged roles.
  • supabase_admin — owns the auth, storage, and realtime schemas. Don't use it interactively. Objects you create get its ownership, and Supabase's own migration scripts assume nobody else has been redecorating.
  • authenticator, supabase_auth_admin, etc. — service roles. Leave them to the services.

For read-heavy team access, create a dedicated read-only role instead of sharing the postgres password — the database roles and permissions guide has a ready-made recipe.

One firm rule: don't hand-edit rows in the auth schema. Changing auth.users directly skips GoTrue's bookkeeping (identities, sessions, audit entries) and produces users that half-work in ways that are miserable to debug. Use the Admin API for user surgery.

Quick error reference

ErrorLikely causeFix
Connection refusedPort not reachable (firewall, or tunnel not established)Verify SSH tunnel is up; check docker compose ps shows db healthy
Tenant or user not foundPlain postgres username sent to Supavisor (6543)Use port 5432 direct, or username postgres.<POOLER_TENANT_ID>
password authentication failed.env changed after first bootPOSTGRES_PASSWORD is baked in at initialization — reset it in Postgres itself, not just .env
prepared statement "S_1" already existsClient connected through transaction-mode poolerSwitch to the direct connection
SSL handshake errorsClient demands verify-full, server has no certsSet client to prefer/require, or configure server TLS properly

Where Supascale fits

None of the above is hard once you know it — but multiply it by every project and every teammate, and "what's the connection string for staging again?" becomes a weekly Slack message. Supascale keeps the connection details for every self-hosted project in one place: host, port, the correct pooler tenant ID, and credentials, per project, without grepping .env files over SSH. And since a GUI client with superuser access is exactly the kind of tool that precedes a "wait, can we restore from last night?" moment, automated scheduled backups with one-click restore are the safety net worth having before someone's UPDATE runs without its WHERE clause.

Wrapping up

Connecting a database GUI to self-hosted Supabase comes down to four decisions made correctly: direct connection on 5432 (not the pooler), SSH tunnel or private network (not an open port), the postgres role (not supabase_admin), and hands off the auth schema. Get those right and pgAdmin, DBeaver, and TablePlus all treat your Supabase stack as what it fundamentally is — a well-organized Postgres database you fully control.

Further Reading