Skip to content

mTLS for managed Postgres: why Supabase and Neon cannot hand you a client certificate

13 min readPostgreSQLSecurityComparisonDatabases

Ask your managed Postgres a simple question: can it verify who is connecting, or only what password they hold? On almost every managed platform the honest answer is the second one. TLS proves to your client that it reached the real server. It does nothing to prove to the server that the client is really you. A leaked connection string, a copied password, or a compromised app server all look identical to a legitimate connection, because the only thing being checked is a shared secret that anyone who has it can present.

Client certificate authentication (mTLS, mutual TLS) closes that gap. Instead of a password alone, the database also demands a certificate signed by a certificate authority it trusts. The client proves possession of a private key that never travels over the wire. Now the server verifies the caller cryptographically, not just the caller's copy of a string. For anyone doing zero-trust networking, or anyone whose compliance reviewer wants "certificate-based authentication" as a literal line item, this is the difference between passing and hand-waving.

The frustrating part, if you have gone looking, is that the big serverless and managed Postgres platforms do not offer inbound mTLS. This is not an oversight they will patch next quarter. It is a direct consequence of how they are built. This post explains why postgres mtls is hard for a managed host, why Supabase and Neon specifically cannot hand you a client certificate today, the one big cloud that does, and how Layerbase Cloud makes serverless postgres mtls a per-database toggle on a $15/mo plan.

TL;DR

  • Server TLS is not mTLS. sslmode=verify-full proves the server to you. It does not prove you to the server. That still leaves a password as the only inbound credential.
  • As of this writing, Supabase, Neon, and PlanetScale do not offer inbound client-certificate authentication. Supabase gives you verify-full and an SSL-enforcement toggle; Neon gives you verify-full and SCRAM channel binding; PlanetScale uses mTLS only for its own import client, not for you.
  • The reason is architectural. These platforms front your database with a shared, multi-tenant TLS-terminating proxy fleet. The TLS handshake ends at the proxy, not at your Postgres, so there is no place to enforce a per-tenant client CA.
  • Google Cloud SQL is the honest exception. It does support client certificates. It is also a very different product to operate, with no free tier and a heavier footprint.
  • Layerbase terminates TLS at your actual database. The connection is passthrough end to end, so Postgres itself performs the handshake and can require a client certificate. You get a per-database CA, one-click rotation, and cert-plus-password enforcement, opt-in per database.

Contents

At a glance

Everything below is accurate as of this writing. Managed platforms change, so verify against current docs if you are making a procurement decision.

SupabaseNeonPlanetScaleGoogle Cloud SQLLayerbase
Client certs for inbound connectionsNoNoNo (import client only)YesYes, Pro plan
Their answer to "verify the caller"Server verify-full plus an SSL enforcement toggleServer verify-full plus SCRAM channel bindingPassword over TLSClient certificatesClient cert plus password, both required
Where TLS terminatesShared TLS-terminating proxy fleetShared proxy (custom serverless routing)Shared Vitess proxyAt the instanceAt your actual database, passthrough
Rotation storyRotate the passwordRotate the password or roleRotate the passwordManage server and client certsOne-click CA rotation revokes every issued cert

Why client certificates, not just a stronger password

If you already enforce TLS and use a long random password, it is fair to ask what a certificate buys you. Three concrete things.

Zero-trust networking. In a zero-trust posture you assume the network is hostile and authenticate every hop. A password is a bearer token: whoever holds it is trusted. A client certificate binds trust to a private key that is provisioned to a specific workload and never leaves it. The database stops trusting "anyone on the network with the string" and starts trusting "the holder of this key." That is the entire premise of postgres mtls managed correctly.

Credential-leak blast radius. Connection strings leak. They end up in a committed .env, a Slack paste, a log line, a screenshot, a former contractor's laptop. When the only credential is a password, a leak is a full compromise until you notice and rotate. When the database also requires a client certificate, a leaked password is inert on its own: the attacker still needs the private key, which is not in the connection string. mTLS turns a single leaked secret from "game over" into "one of two factors gone."

Compliance checkboxes. Plenty of security reviews, vendor questionnaires, and internal standards ask for certificate-based or mutual authentication to data stores by name. IP allowlisting is often explicitly called out as insufficient, because shared egress and rotating cloud IPs make it both leaky and operationally painful. If your reviewer wants mTLS on the database, "we enforce TLS and use a strong password" is not the same answer, and everyone in the room knows it.

The architectural reason managed hosts cannot offer it

Here is the part that explains all the "No" cells above at once. It is not that these teams do not care about security. It is that their architecture puts the TLS handshake in the wrong place for inbound mTLS.

To deliver scale-to-zero, connection pooling, and instant routing, serverless Postgres platforms sit a shared, multi-tenant proxy fleet in front of the actual database. When your client connects, it does a TLS handshake with the proxy. The proxy terminates that TLS, inspects the connection, decides which tenant and which (possibly cold) compute to route to, and opens its own connection to the backend. Your encrypted session ends at the proxy, not at your Postgres.

That design is great for cold starts and pooling. It is fundamentally incompatible with per-tenant client certificates. To verify a client certificate, the endpoint doing the TLS handshake needs your certificate authority loaded so it can check that your client's cert was signed by it. But the handshake endpoint is a shared proxy serving thousands of tenants. There is no clean place to hang a per-database client CA, and asking a multi-tenant TLS terminator to enforce a different trust store per connection is a large, security-sensitive change to the most performance-critical component in the stack. So the feature does not ship.

Layerbase is built the other way. Postgres connections are TLS passthrough end to end. The SNI-based proxy that handles routing and wake-on-connect pipes encrypted bytes straight through; it never terminates your TLS. The handshake happens between your client and your Postgres process itself. Because your database is the endpoint doing the handshake, it can load a certificate authority that is unique to that one database and enforce clientcert in pg_hba.conf natively. Engine-native client-cert auth is feasible for us precisely because we did not put a shared TLS terminator in the path. This is the same reason we can do it on a low-cost plan while the shared-proxy platforms cannot do it at all.

Supabase mTLS: what you actually get

If you search for Supabase mTLS you will find people asking for exactly this, and the current answer is that it is not available for inbound connections. As of this writing Supabase gives you solid server-side TLS: you can connect with sslmode=verify-full and validate Supabase's server certificate, and there is a toggle to enforce SSL so plaintext connections are refused. That protects you from connecting to an impostor and from connecting in the clear.

What it does not give you is inbound client-certificate authentication. Your database still cannot demand a certificate from the connecting client, so the inbound credential remains the password. This is an open community request, not a shipped feature, and the architectural reason above is why it has stayed open: Supabase fronts Postgres with a shared pooler and connection layer that terminates TLS, so there is no per-project place to enforce a client CA.

Neon client certificate support: the same wall

Neon is in the same position for the same reason. Searching for Neon client certificate turns up server-side guidance: connect with verify-full, validate Neon's certificate, and, because Neon speaks SCRAM, you can lean on SCRAM channel binding to tie the password exchange to the TLS channel and frustrate man-in-the-middle attempts. Channel binding is a genuinely good mitigation and worth enabling if you are on Neon.

But channel binding is not mutual authentication. It hardens the password handshake against MITM; it does not let the server require a client certificate. As of this writing Neon does not offer inbound client certificates. Neon's whole value proposition is a custom serverless routing layer in front of Postgres that enables scale-to-zero and branching, and that same routing layer is where the TLS terminates. The feature that makes Neon Neon is the feature that makes serverless postgres mtls unavailable there.

PlanetScale and the mTLS that is not for you

PlanetScale is worth a specific mention because it does say "mTLS" in its docs, and it is easy to misread. The place PlanetScale uses mTLS is database imports, where PlanetScale itself is the TLS client connecting out to your source database. In that flow PlanetScale presents a certificate to your server. That is the opposite direction from what we are discussing. It does nothing for authenticating your application connecting into your PlanetScale database, which as of this writing is still password-over-TLS through the Vitess proxy layer. If a reviewer sees "mTLS" on the PlanetScale site and assumes inbound client-cert auth, that is a trap worth flagging.

The honest Cloud SQL caveat

We are not going to tell you nobody offers this, because that is false. Google Cloud SQL does support client certificates for inbound connections. If your only requirement is "a managed Postgres that accepts a client certificate" and you are already deep in Google Cloud, Cloud SQL is a legitimate answer and you should know it exists.

The trade-offs are the usual Cloud SQL trade-offs. There is no free tier, the smallest instances are not cheap, provisioning and IAM are heavier, and you are operating a full Cloud SQL instance with everything that entails. Client certs there also come with their own lifecycle to manage. It is a real option, and for some teams the right one. It is a different weight class from a per-database toggle on a $15/mo plan, and we would rather you hear that from us than discover it after choosing.

How mTLS works on Layerbase

On Layerbase, mTLS is opt-in per database and reversible. Here is the shape of it.

Every database gets its own certificate authority. When you turn the feature on (or issue your first certificate), Layerbase generates a CA that belongs to that one database. Nothing is shared across tenants or even across your own databases. That per-database CA is what your Postgres loads to verify incoming client certificates.

You issue a certificate, then enable enforcement. The order matters, and it is deliberate: you cannot lock yourself out. From the dashboard you issue a client certificate first. You receive a one-time bundle of three PEM files: your client certificate (client.crt), the matching private key (client.key), and the CA certificate (ca.crt). The private key is generated in memory, handed to you once, and never stored on our side. Download all three at that moment, because the key is not recoverable later. Then you flip enforcement on.

Certificate and password are both required. Enabling mTLS does not replace your password, it stacks on top of it. Every connection must present a valid client certificate signed by that database's CA and pass the normal SCRAM password check. A leaked password with no certificate fails. A certificate with the wrong password fails. You need both.

Connecting looks like a standard psql invocation with the certificate flags filled in:

bash
psql "host=your-db.layerbase.host port=5432 dbname=your_db user=your_user \
  sslmode=verify-full \
  sslrootcert=system \
  sslcert=client.crt \
  sslkey=client.key"

sslmode=verify-full means your client also validates the server, so the trust is mutual in both directions. Note what verifies what: you present client.crt and client.key, and the server checks them against your per-database CA. Your ca.crt is that CA, and it is used server-side only. You do not pass it as sslrootcert, because our Postgres serves a public Let's Encrypt certificate, so your client verifies the server through the operating system trust store (sslrootcert=system, libpq 16+). Point sslrootcert at ca.crt and the handshake fails with certificate verify failed. Set chmod 0600 on client.key; libpq will refuse a world-readable key. The client cert and key work the same way from application drivers (node-postgres, for example, takes ssl: { cert, key, rejectUnauthorized: true } and lets its bundled roots verify the server), which is how you get mTLS from an edge or serverless runtime whose platform does not offer it: load the PEMs from your secret store and connect.

Rotation is revocation. Rotating the CA is one click. It regenerates the database's certificate authority and immediately invalidates every certificate issued under the old one. If a laptop with a client key walks out the door, you rotate, and that key is dead the moment the new CA lands. You then issue fresh certificates to the workloads that should still have access. There is no separate revocation list to manage in v1; rotation is the revocation mechanism.

Pooling keeps working. This is the part that matters for serverless. Enforcement happens at the connection pooler itself: PgBouncer verifies the client certificate on every connection before it reaches Postgres. So you keep the pooled endpoint with mTLS turned on, which is exactly what you want when your app opens a lot of short-lived connections from an edge runtime. Both the pooled and direct connection strings work, and both require the certificate. The query console, backups, and health checks are unaffected.

Full setup, driver snippets, and troubleshooting live in the client certificates docs.

When you do not need this

We would rather you skip a feature you do not need than turn on friction for its own sake. mTLS is the right tool when you have a compliance mandate for certificate-based authentication, when you are building zero-trust networking, or when you connect from an environment (rotating cloud egress IPs, edge functions, shared NAT) where IP allowlisting is either impossible or explicitly not accepted by your policy.

If none of that is you, plain TLS with verify-full and a strong, rotated password is a perfectly reasonable posture for many applications, and it is less to operate. If your real goal is just "only my infrastructure can reach this database," IP allowlisting may be the simpler control, and the two compose: you can require both a source IP range and a client certificate. Pick the control that matches the threat you actually have.

Getting a Postgres with mTLS

If you need a managed Postgres that can verify who is connecting and not just what password they hold, that is the whole reason this feature exists on Layerbase. Client certificate authentication is available on the Pro plan, opt-in per database, with a per-database CA and one-click rotation.

Create a Postgres on Layerbase Cloud, issue a certificate, and enable mTLS from the dashboard. Your database does the TLS handshake itself, so it can demand a certificate the way a shared-proxy platform never can.

Something not working?