The short version
A PostgreSQL connection string is a single URL that tells a client how to reach your database. It looks like this:
postgresql://postgres:[YOUR-PASSWORD]@db.abcdefghij.supabase.co:5432/postgres
Every provider builds on the same format, and the differences that trip people up are almost never the syntax. They're the port and host your provider hands you. Supabase alone has three different strings for the same database, and two of them won't work from serverless apps.
Anatomy of a connection string
postgresql:// user : password @ host : port / database
| | | | | |
protocol username secret server port db name
| Part | Example | Notes |
|---|---|---|
| Protocol | postgresql:// |
postgres:// also works in most clients |
| User | postgres |
In Supabase: postgres.[project-ref] on pooler URLs |
| Password | [your-password] |
URL-encode special characters (@ → %40) |
| Host | db.xxxx.supabase.co |
The server; provider-specific |
| Port | 5432 |
6543 = transaction pooler (Supabase/Neon) |
| Database | postgres |
Default database name |
You can append query parameters after the database. The two you'll actually meet are ?sslmode=require (force TLS) and ?channel_binding=require.
Supabase: three strings for one database
Supabase is where most people hit the connection-string wall, because the right string depends entirely on where your code runs:
| Mode | Host | Port | Best for |
|---|---|---|---|
| Direct | db.[project-ref].supabase.co |
5432 |
Migrations, pg_dump, long-lived backend |
| Shared pooler (session) | aws-[region].pooler.supabase.com |
5432 |
Persistent backend on IPv4-only networks |
| Shared pooler (transaction) | aws-[region].pooler.supabase.com |
6543 |
Serverless / edge / short-lived connections |
# Direct
postgresql://postgres:[YOUR-PASSWORD]@db.[project-ref].supabase.co:5432/postgres
# Shared pooler session mode
postgres://postgres.[project-ref]:[YOUR-PASSWORD]@aws-[REGION].pooler.supabase.com:5432/postgres
# Shared pooler transaction mode (the one most apps want)
postgres://postgres.[project-ref]:[YOUR-PASSWORD]@aws-[REGION].pooler.supabase.com:6543/postgres
The gotcha: direct connections run on IPv6. On an IPv4-only network you get an ENOTFOUND, not a friendly error, just a silent failure to connect. The shared pooler is IPv4-only and fixes it. And if your app is serverless (Vercel functions, edge runtimes, Lambda), use transaction mode (port 6543). It's built for the many-short-connections pattern serverless forces.
This exact confusion is why tools that accept a connection string usually ask you to grab the transaction pooler URL. dbdiagramr, for instance, has a step in its how-it-works guide telling you to select the Transaction pooler in the Supabase dashboard before pasting. Otherwise the connection silently fails from IPv4-only hosts.
Neon: pooled vs direct
Neon makes the two variants explicit in the hostname:
# Pooled (through PgBouncer, use by default)
postgresql://user:pass@ep-cool-rain-123456-pooler.us-east-2.aws.neon.tech/neondb?sslmode=require
# Direct
postgresql://user:pass@ep-cool-rain-123456.us-east-2.aws.neon.tech/neondb?sslmode=require
Rule of thumb: use the -pooler host unless you have a specific reason not to. It handles thousands of concurrent clients and is the right call for serverless. Grab both from the Connect button in the Neon dashboard.
Railway: the plain standard
Railway gives you a no-frills connection string with no pooler decision to make:
postgresql://postgres:[YOUR-PASSWORD]@[host].railway.app:5432/railway
It's the textbook format, which makes it the easiest one to read and the easiest to misplace a password in. Store it in Railway's Variables tab as DATABASE_URL, never in code.
All the formats in one table
| Provider | Connection string shape |
|---|---|
| Standard / Railway | postgresql://user:pass@host:5432/db |
| Supabase (direct) | postgresql://postgres:pass@db.[ref].supabase.co:5432/postgres |
| Supabase (session pooler) | postgres://postgres.[ref]:pass@aws-[region].pooler.supabase.com:5432/postgres |
| Supabase (transaction pooler) | postgres://postgres.[ref]:pass@aws-[region].pooler.supabase.com:6543/postgres |
| Neon (pooled) | postgresql://user:pass@ep-xxx-pooler.region.aws.neon.tech/db?sslmode=require |
| Neon (direct) | postgresql://user:pass@ep-xxx.region.aws.neon.tech/db?sslmode=require |
Gotchas that will actually bite you
-
IPv4 vs IPv6. Supabase direct connections are IPv6. If you're on an IPv4-only network or using a tool on an IPv4-only host, you'll get
ENOTFOUNDand no obvious reason why. The pooler strings are IPv4 and sidestep it. - Serverless wants the transaction pooler. Long-lived backends can hold a connection. Serverless functions can't. Each invocation opens a new one. Session-pooling chokes on that pattern; transaction pooling (port 6543) exists for exactly it.
- A connection string is a secret. It contains your password in plain text. Never commit it, never paste it into a shared doc, and use a pooler/proxy string when a third-party tool needs it. Most good tools never store it beyond the one request. (dbdiagramr introspects your schema and discards the string immediately.)
-
URL-encode passwords. A password containing
@,:, or/breaks the URL. Encode those characters (@→%40) before pasting.
Curious what that pooler URL actually contains after it connects? The Supabase auth schema diagram was created by introspecting a live Supabase database through a transaction pooler string. Users, identities, sessions, and refresh_tokens rendered as relationships.
FAQ
What is a PostgreSQL connection string?
It's a single URL containing everything needed to reach a Postgres database: protocol, username, password, host, port, and database name (e.g. postgresql://user:pass@host:5432/db).
Should I use a direct connection or a pooler?
For a persistent backend that can hold a connection open, direct is fine. For serverless or edge functions that open a connection per invocation, use a pooler, either Supabase transaction mode (port 6543) or a Neon -pooler host.
Why does my Supabase connection fail with ENOTFOUND?
You're probably using the direct connection string over IPv4, and Supabase's direct endpoint is IPv6. Switch to a shared pooler string (aws-[region].pooler.supabase.com) or add the IPv4 add-on.
Is my connection string a secret?
Yes, it's your password in plain text. Keep it in environment variables, rotate it if it leaks, and only hand it to tools that won't store it.
Try It
Live: https://dbdiagramr.space
GitHub: https://github.com/VarunKvK/dbdiagramr
If this is useful to you, a GitHub star helps a solo dev keep building in public. I hit the IPv6 wall the first time I tried to visualize a Supabase database from a serverless app. Figured others would save the hour.
Top comments (1)
Nice breakdown — especially the distinction between direct, session, and transaction pooling.
One thing I’m curious about: when you recommend transaction pooling for serverless, how do you think about session semantics, not just connection count?
Since successive transactions aren't guaranteed to land on the same PostgreSQL backend, things like session-level
SETs, temp tables, advisory locks,LISTEN/NOTIFY, or some prepared-statement strategies can behave differently or break entirely.Do you treat transaction pooling as the default only when the application is deliberately session-stateless, or do you rely on the ORM/driver to abstract those differences away?