You deploy to Deno Deploy, wire up a connection to your Supabase Postgres instance, and the logs spit out: TLS connection failed with message: invalid peer certificate contents: invalid peer certificate: UnsupportedCertVersion. The connection falls back to an unencrypted channel — records are still inserted, but the “success” masks that your data travels in the clear. The immediate fix is the --unsafely-ignore-certificate-errors flag, but keeping encryption enforced requires understanding why Deno’s TLS stack rejects Supabase’s certificate.
- Symptom: TLS connection fails with "UnsupportedCertVersion", fallback to non-encrypted.
- Root cause: Deno’s TLS library (Rustls) refuses Supabase’s certificate version.
-
Fix: Add
--unsafely-ignore-certificate-errorsfor local scripts; for Deploy, use a direct TLS‑disabled Postgres client or proxy. -
Verification: The “Defaulting to non-encrypted connection” log disappears;
pg_stat_sslshows SSL.
The error you see
Here is the exact output that appears in Deno Deploy logs, reported by a developer on Stack Overflow:
TLS connection failed with message: invalid peer certificate contents:
invalid peer certificate: UnsupportedCertVersion
Defaulting to non-encrypted connection
The log appears when you first call any database operation, such as a .create() with the denodb ORM or a raw query with the postgres driver. The connection succeeds regardless — you’ll see rows being created — but behind the scenes Postgres accepted an unencrypted TCP stream, exposing credentials and data on the network.
The code that triggers it (taken from the question) looks like this:
import { Database, PostgresConnector } from "https://deno.land/x/denodb/mod.ts";
import "https://deno.land/x/dotenv/load.ts";
export const connection = (() => {
const DENODB_PGURL = Deno.env.get('DENODB_PGURL');
if (DENODB_PGURL) {
return new PostgresConnector({uri: DENODB_PGURL});
}
const DENODB_HOST = Deno.env.get('DENODB_HOST');
if (!DENODB_HOST) throw new Error('DENODB_HOST is not set');
const DENODB_USERNAME = Deno.env.get('DENODB_USERNAME');
if (!DENODB_USERNAME) throw new Error('DENODB_USERNAME is not set');
const DENODB_PASSWORD = Deno.env.get('DENODB_PASSWORD');
if (!DENODB_PASSWORD) throw new Error('DENODB_PASSWORD is not set');
const DENODB_DATABASE = Deno.env.get('DENODB_DATABASE');
if (!DENODB_DATABASE) throw new Error('DENODB_DATABASE is not set');
return new PostgresConnector({
host: DENODB_HOST,
username: DENODB_USERNAME,
password: DENODB_PASSWORD,
database: DENODB_DATABASE,
});
})()
const db = new Database(connection);
export default db;
No custom TLS options are set, so denodb uses Deno’s default TLS behaviour — which is to enforce strict certificate validation.
Root cause: Deno’s TLS stack vs. Supabase’s certificate version
Deno’s network API delegates TLS to rustls, a Rust library that enforces strict X.509 validation, rejecting any certificate version it does not recognize. During the TLS handshake, rustls inspects the tbsCertificate.version field inside the server’s leaf certificate. If that field contains a value outside the range rustls expects (e.g. a draft version or a deprecated encoding), the handshake is aborted with UnsupportedCertVersion. This mismatch has been observed when connecting to Supabase’s managed PostgreSQL servers from certain Deno runtime versions, particularly on Deno Deploy where the environment cannot be patched by the user. The denodb library’s PostgresConnector does not expose TLS options, so it relies on Deno’s default validation.
The error does not mean the certificate is invalid or the server is malicious — it only means the certificate’s version field does not conform to the TLS library’s expectations. The fallback to “non-encrypted” occurs because the Postgres driver, upon TLS handshake failure, reconnects without encryption as a fallback mechanism. The Deno TLS layer itself terminates the connection when the certificate is invalid; the driver catches that error and attempts a plain TCP connection.
Workaround: --unsafely-ignore-certificate-errors
When you run Deno locally, the quickest way to eliminate the error is to start the script with the --unsafely-ignore-certificate-errors flag. This flag instructs Deno to accept any certificate, regardless of version or trust chain validity:
deno run --allow-net --allow-env --unsafely-ignore-certificate-errors main.ts
With the flag, the TLS connection completes and encryption remains active — the “Defaulting to non-encrypted connection” message disappears because the fallback path is never reached. However, the connection is now blind to any certificate mismatch, including a potential man-in-the-middle attack. Use this flag only on development machines or in throwaway scripts.
On Deno Deploy you cannot pass CLI flags. Use the deno-postgres driver with tls: { enforce: false }. This tells the driver to accept any certificate, including those with unsupported versions, so the TLS handshake succeeds and encryption is maintained. The fallback to an unencrypted connection only occurs if TLS cannot be established at all (for example, if the server does not support TLS), not because of a certificate version mismatch. Therefore, enforce: false is safe and preserves encryption on Deploy; there is no need for a sidecar proxy. The denodb library does not expose a tls option natively, but you can switch to the lower-level deno-postgres driver:
import { Client } from "https://deno.land/x/postgres/mod.ts";
const client = new Client({
hostname: Deno.env.get("DENODB_HOST"),
user: Deno.env.get("DENODB_USERNAME"),
password: Deno.env.get("DENODB_PASSWORD"),
database: Deno.env.get("DENODB_DATABASE"),
tls: { enforce: false }, // accepts any certificate
});
await client.connect();
The enforce: false option accepts any certificate, so the TLS handshake succeeds and encryption is maintained. Unlike the CLI flag, it only affects the PostgreSQL connection and does not disable certificate verification globally. To maintain encryption while bypassing verification locally, use the CLI flag. On Deploy, tls: { enforce: false } is the recommended approach — it preserves encryption and is simpler than a sidecar proxy.
Verify the connection is encrypted
After applying the flag or the tls option, restart your Deno process. The log should no longer contain the fallback message. A quick way to confirm that encryption is active is to run a diagnostic query from within your application:
const result = await client.queryObject`SELECT ssl, version FROM pg_stat_ssl WHERE pid = pg_backend_pid()`;
console.log(result.rows);
Expected output when SSL is on:
[ { ssl: true, version: "TLSv1.3" } ]
If you still see the UnsupportedCertVersion error after these steps, double-check your connection endpoint. The direct database endpoint (db.xxxxx.supabase.co) and the pooler in session mode (port 5432) may present different certificates; some users have found that the pooler's session mode certificate is accepted by Deno. Try using the pooler address on port 5432 (e.g., aws-0-us-xxxx.pooler.supabase.com:5432). Avoid port 6543 (transaction mode) for SSL-sensitive connections. If you prefer to bypass the pooler entirely, use the direct endpoint with port 5432.
FAQ
Is --unsafely-ignore-certificate-errors related to the “peer authentication failed” error?
No. The “peer authentication failed” error happens when Postgres’s pg_hba.conf expects an OS-level user, not a password. Fix: Peer Authentication Failed for User “postgres” covers that situation in detail. The UnsupportedCertVersion error is a pure TLS handshake failure, not an authentication mechanism mismatch.
Can using Supabase’s connection pooler avoid the TLS error?
Yes, the pooler's session mode (port 5432) may present a certificate that Deno accepts. Use the pooler address with port 5432 (e.g., aws-0-us-xxxx.pooler.supabase.com:5432). The pooler's transaction mode uses port 6543, which is the default for pooling, but session mode on port 5432 can also be used. Check your project's connection strings in the Supabase dashboard. The Supabase Connection Pooling guide explains the port numbers and pooling modes in depth.
Why does the connection still work despite the TLS failure?
Deno’s Postgres driver, like many others, falls back to an unencrypted connection when TLS negotiation fails, rather than refusing to connect. Postgres accepts that unencrypted session if the server is configured to allow non-SSL connections. Supabase’s infrastructure permits both SSL and non-SSL connections, so the fallback succeeds silently. The problem is invisible data exposure, not a broken pipeline.
Does this error affect Prisma when used with Deno?
Prisma's CLI commands like prisma migrate dev run on Node.js, not Deno, so they won't encounter this TLS error. However, if you use a Deno-compatible Prisma client (e.g., prisma-client-deno), the client will use Deno's TLS stack and may hit the same UnsupportedCertVersion error when connecting to Supabase. In that case, set the directUrl field in your Prisma datasource to bypass the pooler and connect directly to the database, which may present a different certificate. Use the DATABASE_URL environment variable for the pooled connection and DIRECT_URL for the direct connection, as described in the Prisma documentation.
Related
- Fix: Peer Authentication Failed for User “postgres”
- Supabase Connection Pooling: PgBouncer on Vercel Serverless
- Fix Foreign Key Constraint Violation in Supabase (23503)
- Fix Postgres “Could Not Serialize Access” (40001)
Originally published at https://www.iloveblogs.blog
Top comments (0)