Expose localhost.
Effortlessly.
A typed, event-driven API and CLI over all three Cloudflare tunnel modes — Quick, Remote, and Local. Manages the binary, persists your tunnels, fires typed events. Zero dependencies. Runs on Node 18+ and Bun.
import { TunnelKit } from 'tunnelkit'; const tk = new TunnelKit(); await tk.bin.ensure(); const { publicUrl } = await tk.quick.start({ service: 3000 }); // → https://random-words.trycloudflare.com console.log('Exposed at:', publicUrl);
Everything you need to tunnel,
nothing you don't
A complete, typed wrapper around Cloudflare's three tunnel modes — built to drop into any Node or Bun project without ceremony.
tk.quick · tk.remote · tk.local.TunnelKit and CloudflaredTunnel are typed EventEmitters. Every method, every event — autocomplete, no any.cloudflared on demand, or reuses one already on PATH. Call tk.bin.ensure() for the common case, or pin a version with tk.bin.install().TunnelStore is on by default. Remote and local tunnels you start are saved to config.json so you can restore them on the next boot. Read them back via tk.store.tunnelkit with no args for a full-screen control panel — manage several tunnels at once: [n] new tunnel, [↑/↓] select, [x] stop, [c] copy URL, [m] manage saved.node_modules to audit, no supply-chain to worry about. The library's headline property.status-changed fires on every start/stop and on every edge connection. tk.list() carries live connections[] per tunnel.autoStopMinutes — perfect for demos, webhooks, and ephemeral previews.tk.local.create detects a same-named tunnel on Cloudflare that isn't tracked locally and has no active connections — then cleans it up and retries.CloudflaredTunnel for direct child-process control — url, connected, stdout/stderr events on the raw process.Three modes,
one unified API
One tree, three tunnels
Every mode lives under the namespace that fits it. tk.quick for demos and webhooks, tk.remote for token-based tunnels, tk.local for fully-named tunnels you control. Same patterns, same shapes.
- ✓ Quick:
tk.quick.start/tk.quick.stop - ✓ Remote:
tk.remote.start/tk.remote.stop - ✓ Local:
tk.local.login·create·routeDns·start - ✓
servicecan be a port, a localhost URL, or a full private IP:port
// Quick — no account, random *.trycloudflare.com const { publicUrl } = await tk.quick.start({ service: 3000, autoStopMinutes: 30, }); // Remote — token from Cloudflare dashboard const { ingress } = await tk.remote.start({ id: 'my-app', token: process.env.CF_TUNNEL_TOKEN!, }); // Local — named, full ingress control await tk.local.start({ id: 'storefront', // your handle name: 'acme-prod', // Cloudflare name tunnelId, credentialsFile, ingress: [{ hostname: 'app.example.com', service: 'http://localhost:3000', }], });
A full-screen control panel
Run tunnelkit with no arguments and you get a live TUI for managing every tunnel on the machine. Pick one, stop it, copy its URL, or open breadcrumbed wizards for new and saved tunnels — without leaving the terminal.
- ✓ Listens to
status-changedevents to keep the list in sync - ✓
[↑/↓]select ·[n]new tunnel ·[x]stop ·[c]copy URL ·[m]manage saved - ✓ Breadcrumbed wizards use one-step
Escback navigation and persistent progress logs for create/route/start steps - ✓ Live status dot per tunnel —
●connected ·○disconnected · blinks while connecting - ✓ Confirms before
x/qwhen tunnels have live traffic
tunnelkit > tunnels ❯ ● quick-5173 → https://abc.trycloudflare.com ○ quick-3000 → https://xyz.trycloudflare.com ● remotely-prod → 2 routes - http://localhost:4001 → app.example.com - http://localhost:4002 → api.example.com ● locally-prod → 2 routes - http://localhost:5001 → shop.example.com - http://localhost:5002 → blog.example.com [↑/↓] select [n] new tunnel [x] stop [c] copy URL [m] manage saved [q] quit
Save by default, restore on startup
Remote and local tunnels you start are saved to ~/.tunnelkit/config.json by default (written 0600). Change the location with a custom dataDir, or pass store: false to disable persistence.
- ✓ On by default — no extra setup
- ✓ Quick tunnels are ephemeral and never saved
- ✓ Read saved tunnels back via
tk.storeat any time - ✓ Restore everything on startup with
tk.restoreAll()
import { TunnelKit } from 'tunnelkit'; // Saves to ~/.tunnelkit/config.json by default. const tk = new TunnelKit(); // Inspect what's saved console.log(tk.store.getRemotes()); // → [{ id: 'prod', name: 'prod', token: '***' }] // Restore everything on startup — one call await tk.restoreAll();
Events you can actually subscribe to
A TunnelKit instance is a typed EventEmitter with three high-level events. status-changed is enough for a health view; connection gives per-edge detail (IP, location) as it happens.
- ✓
status-changed— fires on every start, stop, and connection change - ✓
ingress-update— remote tunnel's ingress rules synced from Cloudflare - ✓
connection— an edge connection up/down, withinfo.location - ✓ Low-level
CloudflaredTunnelemitsurl,connected,stdout/stderr
const tk = new TunnelKit(); tk.on('status-changed', (tunnels) => { console.log('Active:', tunnels.map(t => t.id)); }); tk.on('ingress-update', ({ id, ingress }) => { console.log(id, 'ingress:', ingress); }); tk.on('connection', ({ id, info, status }) => { console.log(id, status, info.location.ip); });
The binary is handled for you
Everything shells out to cloudflared. tunnelkit resolves it from the managed installDir (default ~/.tunnelkit/bin), then from PATH. The library never downloads on its own — you call tk.bin.ensure() when you want it. The CLI does it for you on first use.
- ✓
tk.bin.ensure()— resolve, downloading on first use - ✓
tk.bin.status()—{ installed, version, path } - ✓
tk.bin.install('2024.12.2')— pin a specific version - ✓ Reuses an existing
cloudflaredfrombrew/apt/wingetif found
$ tunnelkit install Downloading cloudflared 2024.12.2… ✓ Installed to ~/.tunnelkit/bin/cloudflared $ tunnelkit status binary cloudflared 2024.12.2 path ~/.tunnelkit/bin/cloudflared $ tunnelkit saved ○ storefront local → app.example.com ○ remote-prod remote remote-prod
Install & start tunneling
Requires Node.js v18+ or Bun. The CLI auto-downloads cloudflared on first use, or call tk.bin.ensure() programmatically.
Or as a library: npm install tunnelkit · bun add tunnelkit
Built with zero dependencies
Pure Node built-ins. TypeScript end-to-end. Runs identically on Node 18+ and Bun — no shims, no polyfills, no magic.
Ready to expose
your localhost?
Join developers shipping webhooks, demos, and remote previews with one typed, event-driven API over all three Cloudflare tunnel modes.