pipenet puts a public URL on a local Node server in one command
Expose your local server to the public internet instantly
At a glance
- What is it?
- pipenet is a TypeScript tunnel client and server pair that forwards HTTP, WebSocket and SSE traffic from a public URL to a local port. It is a good fit for demos and webhook testing, and a poor fit for anything that needs TCP traffic or a stable hostname.
- Who is it for?
- pipenet suits front end demos, webhook callbacks and CI steps that need a reachable address for a local process, and it suits teams that want to run the tunnel server themselves on their own domain. It does not suit long-lived production traffic, anything requiring raw TCP, or anyone who needs a hostname they can rely on, because a requested subdomain is not guaranteed.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 45 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A tunnel that only speaks HTTP, and says so
pipenet exists to solve one narrow problem: putting a public address in front of a server running on a laptop. The package description is blunt about it, "Expose your local server to the public internet instantly", and the mechanism is a tunnel client that dials out to a proxy server, which then forwards traffic to the local port.
The interesting decision is the protocol scope. The README carries a table of supported protocols, and only HTTP-based traffic is listed. HTTP and HTTPS are supported as standard request and response. WebSocket is supported through the HTTP upgrade, giving full duplex communication. Server-Sent Events work as long-lived HTTP connections, and HTTP streaming works with chunked transfer encoding.
What is absent is as informative as what is present. There is no mention of raw TCP forwarding, no database protocol support, no UDP. If your local process speaks something other than HTTP, this tool is not the answer, and the absence is a scope decision rather than a roadmap gap you can wait out.
The client is the part you run, the server is optional
The package ships both halves, which is unusual for a tunnel tool. Most comparable setups assume a hosted relay run by the project author, and offer a self-hosted option. pipenet makes both first class. The npm package exposes a root entry point for the client and a separate ./server entry point, and package.json maps the binary to ./dist/cli.js, so one install gives you both the `npx pipenet client` and `npx pipenet server` commands.
The dependency list shows the shape of the implementation. koa and koa-router handle the server side HTTP work, pump moves streams, axios covers the outbound calls the client makes, yargs parses the command line, tldjs deals with public suffix handling when deciding which domain to assign, and human-readable-ids is used to generate the readable identifiers that end up in the public URL.
The project is written in TypeScript with an ES module entry point and requires Node 22 or newer, and the package manager field pins pnpm 10.28.0 for development, although the published package itself is installed with npm in the README.
First tunnel: install the package and point the client at a port
Installing the client is a single npm command, and the README gives no other installation path, so a Node 22 environment with npm is the baseline:
npm install pipenetThe client takes the local port as its main argument. This exposes port 3000 through the default public host, and the client prints the assigned URL once the tunnel is up:
npx pipenet client --port 3000A subdomain can be requested, which is convenient when you want a stable looking name for a demo:
# Request a specific subdomain
npx pipenet client --port 3000 --subdomain myappThe requested name is a request and not a reservation. The option description for `subdomain` states plainly that you may not actually receive this name depending on availability, which is the single most important caveat for anyone planning to put this in a script or a test fixture.
To run against your own relay rather than the hosted one, pass a host:
# Use a custom tunnel server
npx pipenet client --port 3000 --host https://your-tunnel-server.comThe default value of that option is `https://pipenet.dev`, and the same value is used as the default in the JavaScript API.
The JavaScript API returns a tunnel object, not just a URL
Beyond the CLI, the client is usable as a library, which the README recommends for test integration and automation. The function is called as pipenet(port [,options][,callback]). It returns a Promise that resolves once a public tunnel url has been assigned, and it also accepts a single options object with `port` as a property, so both calling styles work:
const tunnel = await pipenet({
port: 3000,
host: 'https://pipenet.dev'
});The resolved value is a tunnel instance, and its `url` property holds the assigned public address. The instance is also an event emitter, with `request`, `error` and `close` events. The request event fires as requests are processed and carries a `method` and `path`, which is enough to write a small test assertion without pulling in a proxy library. The instance has a single method, `close`, which closes the tunnel.
The callback form is described as existing for backwards compatibility with the old Node-style callback API, which tells you the Promise shape is the current one and the callback shape is the legacy one.
Local HTTPS is possible, but the certificate options are on you
The options list is where the client's real flexibility shows. Beyond `port` and `host` there is `localHost`, which proxies to a different hostname instead of localhost and, as a side effect, rewrites the Host header of proxied requests to that value. That Host header rewrite is the detail worth remembering when your local application does virtual host routing.
For a local server that speaks TLS, four further options exist. `localHttps` enables tunneling to a local HTTPS server, `localCert` is the path to a certificate PEM file, `localKey` is the path to the certificate key file, and `localCa` is the path to a certificate authority file for self-signed certificates. The README points at the Node tls.createSecureContext options for details on how those certificate settings are interpreted.
There is also `allowInvalidCert`, a boolean that disables certificate checks for the local HTTPS server and, by the option description, ignores the cert, key and ca options entirely. That is a development convenience with an obvious failure mode: a misconfigured certificate that would otherwise be caught is silently ignored, so a broken local TLS setup can look like a working one until something else depends on the certificate being real.
Running the server yourself, with hooks that see every request
The server half is the part most teams will not use, but it is where the project adds something a hosted service cannot offer: visibility. The server can be started from the CLI with a port, with one or more custom domains, or in a single tunnel port mode:
npx pipenet server --port 3000 --domain tunnel.example.comProgrammatically, createServer takes domains, a secure flag, a landing URL, a socket cap and the tunnel port:
const server = createServer({
domains: ['tunnel.example.com'], // Optional: custom domain(s)
secure: false, // Optional: require HTTPS
landing: 'https://pipenet.dev', // Optional: landing page URL
maxTcpSockets: 10, // Optional: max sockets per client
tunnelPort: 3001, // Optional: shared tunnel port for cloud deployments
});The lifecycle hooks are the reason to self-host. `onTunnelCreated` and `onTunnelClosed` receive an object with an id, a url and a domain, and `onRequest` receives the method, path, tunnelId, headers and remote address of every proxied request. The domain field records which configured domain served the tunnel, which is what makes a multi-domain server trackable. A server operator can build rate limiting or authentication on top of the remote address and headers the request hook provides, and `maxTcpSockets`, defaulting to 10, caps what a single client can hold open.
Cloud deployments need the shared tunnel port option
Deploying the server to fly.io, Docker or Kubernetes runs into a constraint of the default design. The README explains that cloud platforms typically expose only a limited number of ports, while pipenet by default creates a random TCP port for each tunnel client. Random ports per client do not survive that environment.
The fix is the --tunnel-port option, which enables single port mode where every tunnel client connects to one shared port:
# fly.io example
pipenet server --port 8080 --tunnel-port 8081 --domain tunnel.example.com --secureBoth the public port and the tunnel port then have to be exposed in the deployment configuration, and the server must be told to listen on the tunnel port, which the programmatic API does through the server's own tunnel server:
if (server.tunnelServer) {
await server.tunnelServer.listen(3001);
}The server also exposes a small HTTP API: GET /api/status returns server status and a tunnel count, GET /api/tunnels/:id/status returns the status of one tunnel, and GET /:id requests a new tunnel with a chosen identifier. Those endpoints are what a deployment script would use to check on the relay instead of guessing.
Licence, release dates and what the maintenance picture shows
The repository is not archived, and the last push was on 2026-09-07. Releases v1.4.3, v1.4.2 and v1.4.1 all carry dates in 2026, with v1.4.3 on 2026-09-07 and the two v1.4.1 and v1.4.2 builds on 2026-05-13, a short gap apart. The release configuration in package.json relies on semantic-release, so version numbers and changelog entries are produced from commit messages rather than hand written.
The licence needs a separate word, because it is not a standard one. The repository is listed under a custom licence that GitHub cannot classify, and the LICENSE file sits at the repository root alongside the README, tsconfig.json, eslint.config.ts, vitest.config.ts and the source tree. Nothing in the README summarises the terms, so anyone planning to embed pipenet in a product or redistribute the server needs to read that file directly. This is not a legal opinion, just a reminder that a non standard licence is exactly the kind of thing to check before a dependency reaches a release branch.
The version field in package.json reads 1.0.1 while the published releases are in the v1.4 range, so the manifest in the tree and the release tags are not in step, which is a normal artefact of semantic-release managing the published version outside the committed file.
Editorial conclusion
pipenet suits front end demos, webhook callbacks and CI steps that need a reachable address for a local process, and it suits teams that want to run the tunnel server themselves on their own domain. It does not suit long-lived production traffic, anything requiring raw TCP, or anyone who needs a hostname they can rely on, because a requested subdomain is not guaranteed. Before adopting, check the LICENSE file at the repository root yourself, since the licence is a custom one that GitHub cannot classify, and confirm that a single port exposed by your cloud provider is enough to serve every client with the --tunnel-port option.
Frequently asked questions
How do I install pipenet and expose a local port?
Install it with npm install pipenet, then start the client with npx pipenet client --port 3000, which exposes the local port through the public host and prints the assigned URL.
Can I request a specific subdomain with pipenet?
You can ask for one with the --subdomain flag, for example npx pipenet client --port 3000 --subdomain myapp, but the option description warns that you may not receive the requested name depending on availability.
Does pipenet support WebSocket and Server-Sent Events?
Yes. The supported protocol table lists HTTP/HTTPS, WebSocket through the HTTP upgrade, SSE as long-lived HTTP connections, and HTTP streaming with chunked transfer encoding.
Can I run my own pipenet server instead of using pipenet.dev?
Yes, the package includes both client and server components, and you can start a server with npx pipenet server --port 3000 or import createServer from pipenet/server, pointing clients at it with the --host option.
How do I deploy the pipenet server to fly.io?
Use the shared tunnel port mode, for example pipenet server --port 8080 --tunnel-port 8081 --domain tunnel.example.com --secure, and expose both the public port and the tunnel port in the deployment configuration.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/punkpeye-pipenet)Was this useful?