TLS

Serve over HTTPS and, optionally, require a client certificate (mutual TLS).

TLS (the successor of SSL) encrypts the connection between client and server using a certificate and its private key. Serving over HTTPS in srvx is a matter of providing them. Mutual TLS (mTLS) goes one step further and also asks the client to present a certificate, which the server verifies against a trusted CA β€” a common building block for service-to-service and zero-trust setups.

Enabling HTTPS

Provide tls.cert and tls.key. srvx switches the protocol to https automatically.

import { serve } from "srvx";

serve({
  tls: { cert: "./server.crt", key: "./server.key" },
  fetch: () => new Response("πŸ‘‹ Secure hello!"),
});

tls options:

  • cert: Certificate in PEM format β€” file path or inline content (required).
  • key: Private key in PEM format β€” file path or inline content (required).
  • passphrase: Passphrase for the private key (optional).

Server TLS works on Node.js, Deno and Bun.

cert/key/ca values are treated as inline PEM when they start with -----BEGIN , otherwise as file paths.
  • Never commit private keys; load them from environment variables or a secret manager.
  • Consider automatic certificate management (e.g. Let's Encrypt) for production.

Mutual TLS (mTLS)

Mutual TLS is provided by the opt-in mtls() plugin from srvx/mtls. It requests a client certificate during the handshake and exposes it β€” with the negotiated protocol and cipher β€” on request.tls.

There are two places an unauthenticated client can be turned away, depending on rejectUnauthorized:

  • TLS handshake (default, rejectUnauthorized: true) β€” a client without a certificate signed by a trusted ca never completes the handshake. The connection is dropped before any request reaches your fetch handler, so there is nothing to check for in application code.
  • Application layer (rejectUnauthorized: false) β€” every handshake is allowed to complete, and request.tls.authorized tells you whether the presented certificate (if any) was trusted. This is useful when you want to respond with your own error (e.g. a JSON 401) instead of a raw connection reset, or when unauthenticated clients should still reach some routes.
import { serve } from "srvx/node";
import { mtls } from "srvx/mtls";

serve({
  tls: { cert: "./server.crt", key: "./server.key" },
  plugins: [
    mtls({
      ca: "./ca.crt",
      requestCert: true,
      // Accept the handshake even for untrusted/missing certs so the
      // handler can decide how to respond, instead of a TLS-level reset.
      rejectUnauthorized: false,
    }),
  ],
  fetch: (request) => {
    if (!request.tls?.authorized) {
      return new Response("client certificate required", { status: 401 });
    }
    return new Response(`Hello, ${request.tls.peerCertificate?.subject?.CN}`);
  },
});
With the default rejectUnauthorized: true, the if (!request.tls?.authorized) check above is unreachable β€” unauthenticated clients are already rejected at the TLS layer. Only set rejectUnauthorized: false if you intend to enforce authorization yourself in the handler, as shown here.

mtls() options:

  • ca: Trusted CA certificate(s) in PEM format β€” file path(s) or inline content (optional). When set, replaces the well-known Mozilla CAs.
  • requestCert: Request a certificate from connecting clients (default true).
  • rejectUnauthorized: Reject the TLS handshake itself when the client's certificate is not signed by a trusted ca (default true). When false, the handshake always completes and an unverified certificate is instead exposed via request.tls with authorized: false, leaving enforcement to your handler.

request.tls

While the plugin is active, request.tls provides:

  • peerCertificate β€” the client certificate (node:tls PeerCertificate). An empty object ({}) when none was presented.
  • authorized β€” true if the certificate was signed by a trusted ca.
  • authorizationError β€” why verification failed, when authorized is false.
  • protocol β€” negotiated TLS protocol, e.g. "TLSv1.3".
  • cipher β€” negotiated cipher suite.

Runtime support

mtls() requires an HTTPS server (tls.cert + tls.key) and srvx's Node.js adapter (import { serve } from "srvx/node"); it throws otherwise.
  • Node.js β€” works natively.
  • Deno (2.8+) β€” works through the Node adapter, which runs on Deno via node:https. (Native Deno.serve cannot request client certificates.)
  • Bun β€” not currently supported: neither native Bun.serve nor Bun's node:http(s) server exposes the peer certificate to the handler, so the plugin throws. Tracked in oven-sh/bun#16254.