npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gagandeep023/expose-tunnel

v0.4.1

Published

Self-hosted tunnel to expose local servers to the internet. An ngrok/localtunnel alternative you run on your own infrastructure.

Readme

@gagandeep023/expose-tunnel

A self-hosted tunnel to expose local servers to the internet. An alternative to ngrok and localtunnel that runs on your own infrastructure.

npm version license

How It Works

Your Machine                    Your Relay Server                Internet
+--------------+    WebSocket   +------------------------+    HTTPS    +----------+
| localhost:   | -------------> | tunnel.yourdomain.com  | <--------- | Browser  |
| 3000         | <------------- | *.tunnel.yourdomain    | ---------> | requests |
+--------------+                +------------------------+            +----------+
  1. The client connects to your relay server via WebSocket
  2. The relay server assigns a public subdomain (e.g., abc123.tunnel.yourdomain.com)
  3. When external traffic hits that subdomain, the relay server pipes it through the WebSocket to your machine
  4. Your client forwards the request to localhost:<port> and sends the response back

Installation

npm install @gagandeep023/expose-tunnel

Or run directly with npx (no install needed):

npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com --api-key sk_your_key

Quick Start

1. Set environment variables (recommended)

export EXPOSE_TUNNEL_SERVER=wss://tunnel.yourdomain.com
export EXPOSE_TUNNEL_API_KEY=sk_your_key_here

2. Expose a local port

npx @gagandeep023/expose-tunnel --port 3000

Output:

[expose-tunnel] Tunnel established!
[expose-tunnel] Public URL: https://abc123.tunnel.yourdomain.com
[expose-tunnel] Forwarding to: http://localhost:3000
[expose-tunnel] Press Ctrl+C to close the tunnel.

CLI Usage Examples

Basic: expose a port (env vars set)

# Requires EXPOSE_TUNNEL_SERVER and EXPOSE_TUNNEL_API_KEY env vars
npx @gagandeep023/expose-tunnel --port 3000

With a custom subdomain

npx @gagandeep023/expose-tunnel --port 3000 --subdomain myapp
# -> https://myapp.tunnel.yourdomain.com

Without a subdomain (random assigned)

npx @gagandeep023/expose-tunnel --port 3000
# -> https://a1b2c3d4.tunnel.yourdomain.com

With --server flag (no env var needed)

npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com

With --server and --subdomain

npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com --subdomain myapp

With --api-key flag (no env var needed)

npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com --api-key sk_your_key

All flags, no env vars

npx @gagandeep023/expose-tunnel \
  --port 3000 \
  --server wss://tunnel.yourdomain.com \
  --subdomain myapp \
  --api-key sk_your_key_here

Custom local host

# Forward to a different local hostname (default: localhost)
npx @gagandeep023/expose-tunnel --port 3000 --local-host 0.0.0.0

Expose different ports

# Expose a React dev server
npx @gagandeep023/expose-tunnel --port 5173 --subdomain react-app

# Expose an Express backend
npx @gagandeep023/expose-tunnel --port 3001 --subdomain api

# Expose a database admin panel
npx @gagandeep023/expose-tunnel --port 8080 --subdomain admin

CLI Reference

Usage: expose-tunnel [options]

Expose local servers to the internet via your own relay server

Options:
  -V, --version            output version number
  -p, --port <number>      Local port to expose (required)
  -s, --subdomain <name>   Request a specific subdomain
  --server <url>           Relay server WebSocket URL (or set EXPOSE_TUNNEL_SERVER env var)
  --api-key <key>          API key (or set EXPOSE_TUNNEL_API_KEY env var)
  --local-host <host>      Local hostname to proxy to (default: localhost)
  -h, --help               display help for command

Programmatic API

Basic usage

import { exposeTunnel } from '@gagandeep023/expose-tunnel';

const tunnel = await exposeTunnel({
  port: 3000,
  server: 'wss://tunnel.yourdomain.com',
  apiKey: 'sk_your_key_here',
});

console.log(`Public URL: ${tunnel.url}`);

// Close when done
await tunnel.close();

With subdomain

const tunnel = await exposeTunnel({
  port: 3000,
  server: 'wss://tunnel.yourdomain.com',
  apiKey: 'sk_your_key_here',
  subdomain: 'myapp',
});

console.log(tunnel.url);
// -> https://myapp.tunnel.yourdomain.com

Without subdomain (random)

const tunnel = await exposeTunnel({
  port: 3000,
  server: 'wss://tunnel.yourdomain.com',
  apiKey: 'sk_your_key_here',
});

console.log(tunnel.url);
// -> https://a1b2c3d4.tunnel.yourdomain.com

Using env vars (no server/apiKey in code)

// Set EXPOSE_TUNNEL_SERVER and EXPOSE_TUNNEL_API_KEY env vars first
const tunnel = await exposeTunnel({ port: 3000 });

Event listeners

const tunnel = await exposeTunnel({
  port: 3000,
  server: 'wss://tunnel.yourdomain.com',
  apiKey: 'sk_your_key_here',
});

// Log incoming requests
tunnel.on('request', (method, path, status) => {
  console.log(`${method} ${path} -> ${status}`);
});

// Handle errors
tunnel.on('error', (err) => {
  console.error(err);
});

// Handle tunnel close
tunnel.on('close', () => {
  console.log('Tunnel closed');
});

await tunnel.close();

TunnelClient class (advanced)

import { TunnelClient } from '@gagandeep023/expose-tunnel';

const client = new TunnelClient({
  port: 3000,
  server: 'wss://tunnel.yourdomain.com',
  apiKey: 'sk_your_key_here',
  subdomain: 'myapp',
});

const instance = await client.connect();
console.log(instance.url);

// ... use the tunnel

await client.close();

API Reference

exposeTunnel(options)

Creates a tunnel and returns a TunnelInstance.

Parameters:

| Parameter | Type | Required | Default | Description | |-------------|----------|----------|-----------------------------------|--------------------------------------| | port | number | Yes | | Local port to expose | | server | string | Yes* | EXPOSE_TUNNEL_SERVER env var | Relay server WebSocket URL | | apiKey | string | Yes* | EXPOSE_TUNNEL_API_KEY env var | Authentication key | | subdomain | string | No | Random 8-char | Requested subdomain | | localHost | string | No | localhost | Local hostname to proxy requests to |

*Can be provided via env var instead.

Returns: Promise<TunnelInstance>

TunnelInstance

| Property/Method | Type | Description | |-----------------|-------------------------|--------------------------------------------------| | url | string | Public HTTPS URL of the tunnel | | subdomain | string | Assigned subdomain | | close() | () => Promise<void> | Closes the tunnel connection | | on(event, fn) | EventEmitter | Listen for request, error, or close events |

Environment Variables

Client

| Variable | Description | |--------------------------|--------------------------------------------------| | EXPOSE_TUNNEL_SERVER | Relay server WebSocket URL (e.g., wss://tunnel.yourdomain.com) | | EXPOSE_TUNNEL_API_KEY | API key for authenticating with the relay server |

Relay Server

| Variable | Description | Default | |-------------------|----------------------------------------------------------|----------------------------| | RELAY_PORT | Port the relay server listens on | 4040 | | API_KEYS | Comma-separated list of valid API keys | (required) | | TUNNEL_DOMAIN | Base domain for tunnel subdomains | tunnel.gagandeep023.com | | MAX_TUNNELS | Maximum number of concurrent tunnel connections allowed | 10 |

Self-Hosting the Relay Server

The package includes a relay server you can deploy on your own infrastructure. See SELF-HOSTING-GUIDE.md for the full step-by-step deployment guide.

Quick overview

  1. Deploy on any VPS (EC2, DigitalOcean, etc.)
  2. Set up wildcard DNS (*.tunnel.yourdomain.com -> your server IP)
  3. Configure Nginx with wildcard SSL
  4. Start the relay server with PM2

Requirements

  • Node.js 18+
  • A domain with wildcard DNS (e.g., *.tunnel.yourdomain.com)
  • Nginx with wildcard SSL certificate
  • PM2 (recommended) for process management

Quick Setup

mkdir expose-tunnel-server && cd expose-tunnel-server
npm init -y
npm install @gagandeep023/expose-tunnel

Create .env:

RELAY_PORT=4040
API_KEYS=sk_your_generated_key
TUNNEL_DOMAIN=tunnel.yourdomain.com
MAX_TUNNELS=10

Generate an API key:

node -e "console.log('sk_' + require('crypto').randomBytes(24).toString('hex'))"

Create start.js:

const fs = require('fs');
const path = require('path');

const envFile = fs.readFileSync(path.join(__dirname, '.env'), 'utf8');
envFile.split('\n').forEach(line => {
  const [key, ...val] = line.split('=');
  if (key && val.length) process.env[key.trim()] = val.join('=').trim();
});

require('@gagandeep023/expose-tunnel/dist/server/index.js');

Start it:

pm2 start start.js --name expose-tunnel-relay

Health Check

curl https://tunnel.yourdomain.com/health
# { "status": "ok", "tunnels": 0, "maxTunnels": 10 }

TypeScript

Full TypeScript support with exported types:

import type { TunnelOptions, TunnelInstance, TunnelRequest, TunnelResponse } from '@gagandeep023/expose-tunnel';

License

MIT