@socket.dog/qtunnel
v0.4.1
Published
Programmatic tunnels from Node.js/TypeScript plus the qt tunneling CLI
Readme
@socket.dog/qtunnel
Quick tunnels to *.socket.now — from the command line or programmatically
from Node.js/TypeScript applications.
Install
npm install @socket.dog/qtunnel
# or
bun add @socket.dog/qtunnelThe right binary for your platform is installed automatically via optional
dependencies (linux-x64, darwin-arm64, darwin-x64, win32-x64).
Note: the library is ESM-only — use
importto load it. The bundledqt/qtsCLI binaries work everywhere as usual.
CLI
npx qt 3000 # expose localhost:3000
npx qt localhost:5432 # expose a specific host:portLibrary usage
import { createTunnel } from "@socket.dog/qtunnel";
const tunnel = await createTunnel(3000);
console.log(tunnel.urls[0]);
// => https://knowledgeable-amicable-wizard.socket.now
// ...your app serves traffic on port 3000...
await tunnel.close();Tunnels are backed by the bundled qt client and are tied to your app's
lifetime: if your process exits (or is killed with SIGINT/SIGTERM) without you
calling close(), all active tunnels are torn down automatically.
Examples
Expose multiple targets:
const tunnel = await createTunnel({ target: [3000, "localhost:5432"] });
for (const e of tunnel.entries) {
console.log(`${e.url} -> ${e.target}`);
}Wait for readiness explicitly / handle reassignments:
const tunnel = await createTunnel({ target: 8080 });
tunnel.on("update", (urls) => console.log("tunnels:", urls));
tunnel.on("close", ({ code }) => console.log("client exited", code));
// or without awaiting createTunnel's built-in readiness wait:
tunnel.whenReady.then(() => console.log("ready:", tunnel.urls));Use with an Express/Next/etc. server:
import { createTunnel } from "@socket.dog/qtunnel";
import express from "express";
const app = express();
app.get("/", (_req, res) => res.send("hi"));
app.listen(3000, async () => {
const tunnel = await createTunnel(3000);
console.log("public url:", tunnel.urls[0]);
});API
createTunnel(input): Promise<Tunnel>
Creates a tunnel and resolves once public URLs have been assigned.
input is either an options object or just the target(s) —
createTunnel(3000) and createTunnel({ target: 3000 }) are equivalent.
| Option | Type | Default | Description |
| -------------- | ------------------------------------- | ------------------------ | ---------------------------------------------------------------------- |
| target | string \| number \| Array | required | Port (3000), address (localhost:3000, :3000) or a list of these |
| control | string | $QT_CONTROL_ADDR | Control server address override; when unset the config file's control: value (or control.socket.now:443) applies |
| tcp | boolean | false | Force TCP transport instead of QUIC |
| verbose | boolean | false | Verbose logging from the underlying client |
| config | string | $QT_CONFIG_FILE | Path to a qt.yaml config file |
| initIdentity | boolean | true | Run qt init automatically if no identity exists yet |
| cwd | string | process.cwd() | Working directory for the underlying client |
| env | object | – | Extra environment variables |
| timeoutMs | number | 30000 | How long to wait for URL assignment before throwing (0 disables) |
| autoCleanup | boolean | true | Close the tunnel when the process exits |
| binaryPath | string | bundled binary | Override the path to the qt executable |
Throws if the client exits early (e.g. bad identity), cannot be launched, or
no URLs are assigned within timeoutMs.
class Tunnel extends EventEmitter
urls: string[]— current public URLs, e.g.["https://x.socket.now"]entries: {url, target}[]— URL/local-target pairshostnames: string[]— assigned hostnamestargets: readonly string[]— normalized local targetspid: number | undefined— pid of the underlying clientready: boolean,closed: booleanwhenReady: Promise<void>— resolves on first assignment, rejects on failureprocess: ChildProcess— the spawned clientclose(): Promise<void>— idempotent; concurrent calls share one teardown and resolve once the client has exited
Events:
"ready"— first URL assignment"update"—(urls: string[])assignments changed after a reconnect"stderr"—(chunk: string)diagnostic output from the client"close"—({code, signal})the underlying client exited
Helpers
getBinaryPath()— resolved location of the bundledqtbinary (override with theQT_BINARYenvironment variable)normalizeTarget(target)—3000→"localhost:3000"
Notes
- Identity: the first run creates a local keypair via
qt init(passinitIdentity: falseto manage this yourself). Tunnels are stable per identity + target. - Multiple targets: pass them in one call to share a single connection.
If you need a specific domain for a specific target, use one
createTunnel()call per target. - App lifetime: once ready, the tunnel never keeps your event loop alive;
when your app exits, exit hooks terminate the client (a graceful SIGTERM,
which
qthandles by closing its connection cleanly). If your app installs no signal handlers of its own, auto-cleanup closes all tunnels on SIGINT/SIGTERM/SIGHUP and the process terminates with the conventional128+Nstatus code (a second signal force-exits). If your app does install handlers, the library still tears the tunnels down on those signals but leaves the exit decision to your handlers. The library's own handlers (and theexithook) are removed once no tunnels are active, so default signal behaviour is restored. SetautoCleanup: falseto opt out entirely and callclose()yourself.
License
See the repository for license information.
