@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.
Maintainers
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.
How It Works
Your Machine Your Relay Server Internet
+--------------+ WebSocket +------------------------+ HTTPS +----------+
| localhost: | -------------> | tunnel.yourdomain.com | <--------- | Browser |
| 3000 | <------------- | *.tunnel.yourdomain | ---------> | requests |
+--------------+ +------------------------+ +----------+- The client connects to your relay server via WebSocket
- The relay server assigns a public subdomain (e.g.,
abc123.tunnel.yourdomain.com) - When external traffic hits that subdomain, the relay server pipes it through the WebSocket to your machine
- Your client forwards the request to
localhost:<port>and sends the response back
Installation
npm install @gagandeep023/expose-tunnelOr run directly with npx (no install needed):
npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com --api-key sk_your_keyQuick Start
1. Set environment variables (recommended)
export EXPOSE_TUNNEL_SERVER=wss://tunnel.yourdomain.com
export EXPOSE_TUNNEL_API_KEY=sk_your_key_here2. Expose a local port
npx @gagandeep023/expose-tunnel --port 3000Output:
[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 3000With a custom subdomain
npx @gagandeep023/expose-tunnel --port 3000 --subdomain myapp
# -> https://myapp.tunnel.yourdomain.comWithout a subdomain (random assigned)
npx @gagandeep023/expose-tunnel --port 3000
# -> https://a1b2c3d4.tunnel.yourdomain.comWith --server flag (no env var needed)
npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.comWith --server and --subdomain
npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com --subdomain myappWith --api-key flag (no env var needed)
npx @gagandeep023/expose-tunnel --port 3000 --server wss://tunnel.yourdomain.com --api-key sk_your_keyAll flags, no env vars
npx @gagandeep023/expose-tunnel \
--port 3000 \
--server wss://tunnel.yourdomain.com \
--subdomain myapp \
--api-key sk_your_key_hereCustom local host
# Forward to a different local hostname (default: localhost)
npx @gagandeep023/expose-tunnel --port 3000 --local-host 0.0.0.0Expose 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 adminCLI 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 commandProgrammatic 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.comWithout 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.comUsing 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
- Deploy on any VPS (EC2, DigitalOcean, etc.)
- Set up wildcard DNS (
*.tunnel.yourdomain.com-> your server IP) - Configure Nginx with wildcard SSL
- 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-tunnelCreate .env:
RELAY_PORT=4040
API_KEYS=sk_your_generated_key
TUNNEL_DOMAIN=tunnel.yourdomain.com
MAX_TUNNELS=10Generate 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-relayHealth 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
