Skip to main content

Guide

listhen vs local-ssl-proxy vs mkcert 2026

Compare listhen, local-ssl-proxy, and mkcert for running HTTPS locally during development. Self-signed certificates, trusted CA, dev server setup, and which.

·PkgPulse Team·
0

TL;DR

listhen is the UnJS HTTP server launcher — starts a local server with auto HTTPS (self-signed certs), clipboard URL copy, open-in-browser, graceful shutdown, and tunnel support. local-ssl-proxy creates an HTTPS proxy in front of your HTTP dev server — run your app on HTTP, proxy adds SSL termination. mkcert generates locally-trusted SSL certificates — creates a local CA, installs it in your system trust store, no browser warnings. In 2026: listhen for quick dev server launches with auto HTTPS, mkcert for trusted certificates in any setup, local-ssl-proxy for adding HTTPS to existing HTTP servers.

Key Takeaways

  • listhen: ~5M weekly downloads — UnJS, auto HTTPS, dev server launcher, tunnel support
  • local-ssl-proxy: ~50K weekly downloads — HTTPS proxy, wraps any HTTP server
  • mkcert: CLI tool (Go binary) — creates trusted local certificates, no browser warnings
  • The problem: many APIs require HTTPS (OAuth, cookies, WebAuthn, geolocation)
  • Self-signed certs trigger browser warnings — mkcert creates truly trusted certs
  • listhen is the quickest path: npx listhen ./handler.ts → HTTPS server running

Why Local HTTPS?

APIs that require HTTPS:
  🔒 OAuth 2.0 callbacks — most providers require HTTPS redirect URIs
  🔒 Secure cookies — SameSite=None requires Secure flag (HTTPS only)
  🔒 WebAuthn/Passkeys — only works on HTTPS origins
  🔒 Geolocation API — requires secure context
  🔒 Service Workers — only register on HTTPS
  🔒 Web Crypto API — some features require secure context
  🔒 Clipboard API — requires secure context

Development without HTTPS:
  localhost is treated as secure context in most browsers
  But subdomain.localhost or custom domains are NOT secure
  And API callbacks need real HTTPS URLs

listhen

listhen — dev server launcher:

Quick start

# Start a server with auto HTTPS:
npx listhen ./server.ts

# Output:
# ✅ Listening on:
#   → http://localhost:3000
#   → https://localhost:3001
# 📋 URL copied to clipboard

# With options:
npx listhen ./server.ts --port 8080 --https --open

Programmatic usage

import { listen } from "listhen"

// Start server with handler:
const listener = await listen(
  (req, res) => {
    res.end("Hello from HTTPS!")
  },
  {
    port: 3000,
    https: true,        // Auto-generate self-signed cert
    open: true,          // Open browser
    clipboard: true,     // Copy URL to clipboard
    showURL: true,       // Print URL to console
  }
)

console.log(listener.url)    // http://localhost:3000
console.log(listener.https?.url)  // https://localhost:3001

// Graceful shutdown:
await listener.close()

With H3/Nitro handler

import { listen } from "listhen"
import { createApp, eventHandler, toNodeListener } from "h3"

const app = createApp()
app.use("/api/health", eventHandler(() => ({ status: "ok" })))
app.use("/api/data", eventHandler(() => ({ packages: 466 })))

const listener = await listen(toNodeListener(app), {
  https: true,
  port: 3000,
})

Tunnel support

import { listen } from "listhen"

// Expose local server to the internet:
const listener = await listen(handler, {
  tunnel: true,  // Creates a public tunnel URL
  // → https://abc123.tunnel.dev
})

console.log(listener.tunnel?.url)
// Useful for: webhook testing, mobile testing, sharing demos

Certificate options

import { listen } from "listhen"

// Auto self-signed (default):
await listen(handler, { https: true })

// Custom certificate:
await listen(handler, {
  https: {
    cert: "./certs/localhost.pem",
    key: "./certs/localhost-key.pem",
  },
})

// Use mkcert certificates for trusted HTTPS:
await listen(handler, {
  https: {
    cert: "./certs/localhost+2.pem",     // mkcert generated
    key: "./certs/localhost+2-key.pem",
  },
})

mkcert

mkcert — trusted local certificates:

Setup

# Install mkcert:
# macOS:
brew install mkcert

# Windows:
choco install mkcert

# Linux:
# Download from GitHub releases

# Install local CA (one-time setup):
mkcert -install
# → Created a new local CA 🎉
# → The local CA is now installed in the system trust store

Generate certificates

# Generate cert for localhost:
mkcert localhost
# → localhost.pem, localhost-key.pem

# Multiple domains:
mkcert localhost 127.0.0.1 ::1 myapp.local
# → localhost+3.pem, localhost+3-key.pem

# Wildcard:
mkcert "*.local.dev" local.dev localhost
# → _wildcard.local.dev+2.pem, _wildcard.local.dev+2-key.pem

Use with Node.js

import https from "node:https"
import { readFileSync } from "node:fs"

// Use mkcert certificates — NO browser warnings:
const server = https.createServer({
  cert: readFileSync("./certs/localhost+2.pem"),
  key: readFileSync("./certs/localhost+2-key.pem"),
}, (req, res) => {
  res.end("Trusted HTTPS!")
})

server.listen(3000)
// Browser shows 🔒 padlock — no warning!

Use with Express/Fastify

import express from "express"
import https from "node:https"
import { readFileSync } from "node:fs"

const app = express()
app.get("/", (req, res) => res.json({ secure: true }))

const server = https.createServer({
  cert: readFileSync("./localhost+2.pem"),
  key: readFileSync("./localhost+2-key.pem"),
}, app)

server.listen(3000, () => {
  console.log("Trusted HTTPS on https://localhost:3000")
})

Use with Vite

// vite.config.ts
import { readFileSync } from "node:fs"
import { defineConfig } from "vite"

export default defineConfig({
  server: {
    https: {
      cert: readFileSync("./certs/localhost.pem"),
      key: readFileSync("./certs/localhost-key.pem"),
    },
  },
})

Why mkcert is special

Self-signed certificates:
  ❌ Browser shows "Your connection is not private"
  ❌ Must click "Advanced → Proceed" every time
  ❌ Some APIs refuse to work (Service Workers, WebAuthn)

mkcert certificates:
  ✅ Browser shows green padlock 🔒
  ✅ No warnings — cert is trusted by your system
  ✅ All HTTPS-only APIs work correctly
  ✅ One-time CA install — all future certs are trusted

local-ssl-proxy

local-ssl-proxy — HTTPS proxy:

Quick usage

# Your app runs on HTTP:
node server.js  # Listening on http://localhost:3000

# In another terminal, proxy with HTTPS:
npx local-ssl-proxy --source 3001 --target 3000

# Now both work:
# http://localhost:3000   → Your app (HTTP)
# https://localhost:3001  → Your app (HTTPS via proxy)

With custom certificates

# Use mkcert certs for trusted proxy:
npx local-ssl-proxy \
  --source 3001 \
  --target 3000 \
  --cert ./localhost.pem \
  --key ./localhost-key.pem

package.json setup

{
  "scripts": {
    "dev": "node server.js",
    "dev:ssl": "concurrently \"npm run dev\" \"local-ssl-proxy --source 3001 --target 3000\""
  }
}

When to use local-ssl-proxy

Best for:
  ✅ Adding HTTPS to an app you can't modify
  ✅ Quick HTTPS for testing (no code changes)
  ✅ Proxying any HTTP server (Express, Fastify, Django, Rails)

Limitations:
  ❌ Extra process running
  ❌ Different port (3001 vs 3000)
  ❌ Self-signed certs by default (use mkcert for trusted)
  ❌ Not needed if your framework has HTTPS built-in

Feature Comparison

Featurelisthenmkcertlocal-ssl-proxy
Starts server❌ (certs only)❌ (proxy only)
Auto self-signed❌ (trusted)✅ (self-signed)
Trusted certs❌ (self-signed)❌ (self-signed)
Custom certsGenerates them
Tunnel (public URL)
Open browser
Clipboard copy
Works with any server❌ (Node.js)
Code changes neededYes (handler)Yes (cert config)No
LanguageJavaScriptGoJavaScript
Weekly downloads~5MCLI tool~50K

# Best combo: mkcert + your framework's HTTPS support

# 1. One-time: install mkcert and generate certs
mkcert -install
mkcert localhost 127.0.0.1 ::1

# 2. Use certs in your dev server (Vite, Next.js, etc.)
# Or with listhen:
npx listhen ./handler.ts --https.cert localhost+2.pem --https.key localhost+2-key.pem

When to Use Each

Use listhen if:

  • Need a quick dev server with auto HTTPS
  • Building with H3/Nitro (UnJS ecosystem)
  • Want tunnel support for webhook testing
  • Need clipboard copy and auto-open browser

Use mkcert if:

  • Need trusted HTTPS certificates (no browser warnings)
  • Working with OAuth, WebAuthn, or Service Workers
  • Want to use trusted certs with any framework
  • One-time setup — all future certs are trusted

Use local-ssl-proxy if:

  • Need to add HTTPS to a server you can't modify
  • Quick testing without any code changes
  • Proxying non-Node.js servers (Python, Ruby, Go)
  • Don't want to configure HTTPS in your app

Team Certificate Management with mkcert

mkcert installs a local Certificate Authority into the operating system's trust store, which means the trust is per-machine, not per-project. When a developer installs mkcert and runs mkcert -install, their machine trusts all certificates signed by that CA — including future certificates generated for new projects or new domains. This is exactly the right model for individual workstations.

The challenge arises in team environments. Each developer runs their own mkcert -install and generates their own certificates, producing per-developer CA and cert files. These should never be committed to the repository — the private key of your local CA is a sensitive secret that should not live in git history. The correct approach is to document the setup in your project README and use a local certificate path that is gitignored (e.g. certs/ in your project root).

For CI environments that need to test HTTPS locally, mkcert's -CAROOT flag allows specifying a custom CA root directory. A team can generate a shared CA for CI (stored in the CI secrets manager, not in git) and provision certificates in the CI setup step. Alternatively, self-signed certificates with NODE_TLS_REJECT_UNAUTHORIZED=0 are acceptable for CI HTTPS testing since the certificate trust is not important when you control both sides of the connection — just document clearly that this flag must never appear in production code.

Docker-based development environments present a different challenge: the mkcert CA installed on the host is not trusted inside the container. The solution is to copy the CA root certificate from mkcert -CAROOT into the container image and run update-ca-certificates (Debian/Ubuntu) or trust anchor (Fedora) to make the container trust it. Once done, HTTPS requests from inside the container to localhost resolve correctly with the mkcert certificate.


OAuth and Webhook Testing Without Public Tunnels

One of the most concrete reasons developers need local HTTPS is OAuth redirect URI validation. Auth providers like GitHub, Google, and Okta verify that the redirect_uri parameter in an OAuth flow exactly matches a URI registered in the developer console. For local development, you register https://localhost:3000/auth/callback, which requires your local server to actually serve HTTPS on port 3000.

listhen covers this use case directly when you pass a custom certificate. Combined with mkcert-generated trusted certificates, listen(handler, { https: { cert, key }, port: 3000 }) produces an HTTPS server on the exact port your OAuth app is registered for, with a browser-trusted certificate, in roughly 10 lines of setup code. This is significantly faster than configuring nginx locally or fighting with self-signed certificate acceptance dialogs.

Webhook testing has a similar requirement — many SaaS providers (Stripe, GitHub, Twilio) will only POST to HTTPS endpoints. local-ssl-proxy is the right tool here when your webhook handler is already running as an HTTP server that you cannot modify. Starting local-ssl-proxy --source 3001 --target 3000 in a second terminal window and registering https://localhost:3001/webhook in the Stripe dashboard takes under a minute. The proxy transparently forwards the decrypted body to your HTTP handler, which can remain HTTPS-unaware entirely.

For sharing local servers with remote collaborators or testing on mobile devices on the same network, listhen's tunnel feature creates a publicly accessible HTTPS URL backed by a managed tunneling service. This eliminates the need to configure ngrok separately, which is particularly convenient in monorepo setups where multiple developers may need simultaneous tunnels for different microservices running on different local ports.


Certificate Rotation and Expiry Management

mkcert-generated certificates have a default validity period of 825 days — just under two and a half years. For active development machines, this means you will eventually need to regenerate certificates. The regeneration process is simple: run mkcert localhost 127.0.0.1 ::1 again in your project directory to generate new certificates with a fresh validity period. The local CA installed by mkcert -install does not expire during the certificate's lifetime for typical usage, but if you reinstall your operating system or clear system trust stores, you will need to run mkcert -install again to reinstall the CA. Teams should document the mkcert CA root location (mkcert -CAROOT prints it) in their development setup guide, so new team members know whether to install the shared team CA or generate their own. For Docker-based development, pinning the certificate in a volume mount with a clear expiry date in the filename (e.g., localhost-cert-2027.pem) makes rotation visible rather than silent.

HTTPS and Service Worker Registration in Development

Service Workers require an HTTPS origin or localhost specifically — any other hostname, including custom local domains like myapp.local, fails the secure context check that browsers enforce before allowing service worker registration. This becomes relevant when testing Progressive Web App features, offline caching, push notifications, or any feature that depends on the service worker lifecycle. Running on localhost:3000 avoids the HTTPS requirement for service workers, but custom local domains do not get this exception. If your development environment uses a custom hostname (common in microservice setups or when testing multi-tenant subdomains like tenant1.myapp.local), mkcert is the only option that provides a browser-trusted certificate for that hostname. Generate a wildcard certificate with mkcert "*.myapp.local" myapp.local localhost and configure your dev server to use it — service workers will then register correctly on all those origins. listhen's auto-generated self-signed certificates trigger the browser's "not secure" warning, which blocks service worker registration on non-localhost origins despite the HTTPS scheme.

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on listhen v1.x, mkcert v1.x, and local-ssl-proxy v2.x.

Compare development server and HTTPS tooling on PkgPulse →

The right choice depends on where you need HTTPS. listhen handles the Node.js HTTP server setup automatically and is the zero-config path for Nitro/Nuxt developers. local-ssl-proxy adds HTTPS as a proxy layer in front of any existing server, without touching the server code. mkcert solves the certificate trust problem permanently, working with any server or tool that accepts a certificate file.

See also: cac vs meow vs arg 2026 and cosmiconfig vs lilconfig vs conf, archiver vs adm-zip vs JSZip (2026).

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.