Skip to content

Postgres mTLS from Supabase Edge Functions: a step-by-step guide

5 min readPostgreSQLSecurityServerlessTutorials

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.crt and client.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:

bash
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

Use postgres.js via npm:postgres. Its ssl option accepts standard Node tls.connect fields, including the client certificate. The deno.land/x/postgres driver only supports caCertificates and cannot present a client certificate, so it will not work for mTLS.

typescript
import postgres from 'npm:postgres'

// The cert + key come from function secrets, never files in the repo.
// Do NOT set ssl.ca to your per-database ca.crt: that CA verifies YOUR
// certificate, not our server. Layerbase Postgres serves a public Let's
// Encrypt certificate, so with ssl.ca omitted the runtime verifies the
// server against its bundled roots and rejectUnauthorized: true.
const sql = postgres({
  host: 'your-db.cloud.layerbase.com',
  port: 5432,
  database: 'app',
  username: 'app_user',
  password: Deno.env.get('DB_PASSWORD'),
  ssl: {
    cert: Deno.env.get('DB_CERT'),
    key: Deno.env.get('DB_KEY'),
    rejectUnauthorized: true,
  },
})

const [row] = await sql`select now() as ts`

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 ssl.ca). 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. In postgres.js you get this for free by omitting ssl.ca with rejectUnauthorized: true. With psql you use sslmode=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.

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.

Something not working?

Postgres mTLS from Supabase Edge Functions (client certificates) | Layerbase