Postgres mTLS from Supabase Edge Functions: a step-by-step guide
Short version: an IP allowlist cannot pin a Supabase Edge Function, because it egresses from a pool of shared, rotating addresses that are not even yours alone, which leaves a password in an environment variable as the only thing guarding the database. Client-certificate auth is the control that fits, because it proves the caller cryptographically and does not care where the connection came from. Supabase's own Postgres does not offer inbound mTLS, so this guide points the function at a Layerbase Postgres that does: issue a certificate, turn enforcement on, store the PEMs as function secrets, and connect. The one Deno-specific catch is that Deno.startTls silently drops the cert and key options, so a stock Deno Postgres driver never presents a certificate at all; the @layerbase/deno-mtls adapter uses the direct-TLS endpoint instead and hands the socket to node-postgres. Pooling stays on, because enforcement happens at the pooler rather than by forcing direct connections.
If you run Supabase Edge Functions against a Postgres database, you have probably hit the wall this guide is about. Edge functions connect from a pool of shared, rotating egress IPs. You cannot pin them with an IP allowlist, because the address changes and it is not even yours alone. So the only thing standing between your database and the whole internet is a password in an environment variable. For a lot of teams, and every compliance reviewer, that is not enough.
The fix is client-certificate authentication, also called mTLS. The database demands a certificate signed by a CA it trusts, in addition to the password. The certificate proves the caller cryptographically, and it does not depend on where the connection comes from, which is exactly why it works from serverless where IP allowlisting cannot.
The catch: Supabase's own Postgres does not offer inbound mTLS, and neither do Neon or PlanetScale (the architectural reason is their shared TLS-terminating proxy fleets). But you do not have to give up edge functions to get it. You can point your Supabase Edge Function at a Layerbase Postgres database that enforces mTLS, and keep everything else about your stack. Here is how, end to end.
What you will end up with
- A Postgres database that rejects any connection without a valid client certificate, on top of the password.
- A per-database certificate authority you control. Rotate it in one click and every issued certificate dies instantly.
- Your Supabase Edge Function connecting with the certificate stored as function secrets, not files.
- Connection pooling still on. This matters for edge functions, which open many short-lived connections. Enforcement happens at the pooler, so you keep the pooled endpoint.
Step 1: Create a Postgres database
Create a PostgreSQL database from the Layerbase dashboard. You get a connection string immediately. Client-certificate auth is a Pro-plan feature, so make sure the database is on Pro before the next step.
Step 2: Issue a client certificate
Open the database, go to the Security tab, and click Issue certificate. You get a one-time bundle with three files:
client.crtandclient.key: the certificate and private key your function will present.ca.crt: the certificate authority that signed your client certificate. You do not connect with this file (more on that below), it is there for your records.
Download all three now. The private key is shown only once. If you lose it, rotate the CA and issue a new one.
Step 3: Turn on enforcement
Back on the Security tab, flip Require client certificate on. From this moment, every connection to the database needs both a valid certificate and the password. Existing connections without a certificate are rejected on their next connect, so cut over your function in the same window.
Step 4: Store the certificate as function secrets
Do not commit the certificate to your repo or bake it into the function image. Store it as Supabase secrets:
supabase secrets set \
DB_CERT="$(cat client.crt)" \
DB_KEY="$(cat client.key)" \
DB_PASSWORD="your-database-password"Step 5: Connect from the edge function
Here is the part that trips people up. Deno negotiates Postgres TLS with a STARTTLS upgrade (Deno.startTls), and that call silently drops the cert and key options, so no client certificate is ever presented. A stock Deno Postgres driver (including postgres.js over STARTTLS, and the deno.land/x/postgres driver) therefore cannot do mTLS: the server rejects the connection with "certificate required."
The fix is the @layerbase/deno-mtls adapter. It connects to the Layerbase direct-TLS endpoint, where TLS starts on the first byte (Deno.connectTls, which does present the client cert), then hands the socket to node-postgres for SCRAM auth and queries. You get back a normal pg client. Point port at the direct-TLS port shown in the Connect dialog (5433 by default).
import { connect } from 'jsr:@layerbase/deno-mtls'
// The cert + key come from function secrets, never files in the repo.
// You do NOT pass your per-database ca.crt: that CA verifies YOUR
// certificate, not our server. Layerbase Postgres serves a public Let's
// Encrypt certificate, so Deno's built-in roots verify the server.
const client = await connect({
hostname: 'your-db.cloud.layerbase.com',
port: 5433, // the direct-TLS port from the Connect dialog
database: 'app',
user: 'app_user',
password: Deno.env.get('DB_PASSWORD'),
cert: Deno.env.get('DB_CERT'), // full client.crt PEM
key: Deno.env.get('DB_KEY'), // full client.key PEM
})
const { rows } = await client.query('select now() as ts')
await client.end()That is the whole change. Your function now authenticates with a certificate, and a leaked password alone can no longer connect.
The one thing people get wrong
The most common mistake is passing the per-database ca.crt as the client's sslrootcert (or as caCerts). That fails with certificate verify failed, and it is worth understanding why. There are two different certificate authorities in play:
- The server's CA is a public one (Let's Encrypt). Your client uses it, or the system trust store, to verify that it reached the real Layerbase server. With the
@layerbase/deno-mtlsadapter you get this for free by omittingcaCerts(Deno's bundled roots verify the server). Withpsqlyou usesslmode=verify-full sslrootcert=system. - Your database's CA (
ca.crt) is private, and the server uses it to verify your client certificate. You never pass it on the client side.
Keep those straight and connections work the first time.
Why pooling still works
Edge functions are the worst case for connection count: many invocations, each opening a short-lived connection. That is exactly where a pooler earns its keep, and it is why some approaches to mTLS (turning the pooler off and forcing direct connections) are a poor fit for serverless. Layerbase enforces the certificate at the pooler itself, so you keep the pooled endpoint with mTLS on. Nothing about your connection-count profile changes when you turn enforcement on.
Rotating and revoking
If a key leaks, or a function is decommissioned, rotate the CA from the Security tab. It regenerates the database's certificate authority and immediately invalidates every certificate issued under the old one, then hands you a fresh bundle to redeploy. There is no separate revocation list to manage; rotation is the revocation mechanism.
When you do not need this
If your database is reached only from a fixed set of servers with stable IPs, IP allowlisting alone may be all you need, and it is simpler. mTLS is the right tool specifically when the caller's network identity is unstable (serverless, rotating egress) or when a policy requires client-side certificate authentication regardless of network location.
FAQ
Why can I not just IP-allowlist my edge functions?
Because the address you would allowlist is neither stable nor exclusively yours. Edge functions egress from a shared, rotating pool, so any range wide enough to cover your function also covers everyone else running on the same infrastructure. That is not a control your compliance reviewer will accept, and it is not one that meaningfully narrows the blast radius of a leaked connection string either.
My Deno function connects but the server says a certificate is required. What is wrong?
Nothing in your code, most likely. Deno negotiates Postgres TLS with a STARTTLS upgrade through Deno.startTls, and that call silently drops the cert and key options, so no certificate is ever presented. That applies to stock Deno Postgres drivers generally, including postgres.js over STARTTLS and deno.land/x/postgres. Use the @layerbase/deno-mtls adapter against the direct-TLS port from the Connect dialog, where TLS starts on the first byte and the certificate does get sent.
Do I need ca.crt in the function?
No. There are two certificate authorities pointing in opposite directions, and mixing them up is the most common failure here. Your per-database ca.crt is what the server uses to check your client certificate, so it never goes on the client side. The server presents a public Let's Encrypt certificate, which Deno's bundled roots verify for free when you omit caCerts. Pass ca.crt as sslrootcert or caCerts and you get certificate verify failed.
Will enforcement break connection pooling?
No, and that was the design constraint. Some approaches to mTLS require turning the pooler off and connecting directly, which is a bad fit for edge functions precisely because they open many short-lived connections. Layerbase verifies the certificate at the pooler, so you keep the pooled endpoint and your connection-count profile does not change when you flip enforcement on.
What do I do if a function's key leaks?
Rotate the CA from the Security tab. It regenerates the database's certificate authority and immediately invalidates every certificate issued under the old one, then hands you a fresh bundle to redeploy. Do the same when a function is decommissioned rather than leaving a live certificate in a secret store nobody is watching. There is no separate revocation list; rotation is the revocation mechanism.
Get started
Full reference, including the psql and node-postgres variants, is in the client certificates docs. To try it, create a Postgres database, issue a certificate, and point your edge function at it. Fixed pricing, pooling included, and mTLS is a per-database toggle.
Keep reading
- Postgres over HTTP for Vercel, Workers, and Lambda: the Neon serverless driver now works on LayerbaseEvery Layerbase Postgres now speaks the HTTP protocol used by @neondatabase/serverless. Pass your normal connection string to neon() and query from Vercel Functions, Cloudflare Workers, or AWS Lambda with no TCP socket, no VPC, and no RDS Proxy. Here is what works, what does not, and how it is built.
- The SSL modes 'prefer', 'require', and 'verify-ca' are treated as aliases for 'verify-full': what this warning meansIf your Node app just started printing a SECURITY WARNING about SSL modes being treated as aliases for verify-full, nothing is broken and nothing has changed yet. Here is what the warning actually means, why it appeared out of nowhere, and the one-line connection string fix.
- Serverless Postgres: the options in 2026Neon, Aurora Serverless v2, Supabase, PlanetScale Postgres, and Layerbase all answer to "serverless Postgres" and mean different things by it. What each one actually does at idle, how you connect from a serverless runtime, and where each one is the right pick.
- Every Free Database Tier That Sleeps, Pauses, or Expires - and What Staying Awake Actually CostsA database that scale-to-zeros after five minutes, a project that pauses after a week and needs a human to click Resume, and a database that gets deleted 44 days after you created it are three different products. Here is which vendor does which, and what the cheapest always-on version costs.