Documentation

Everything DevLink365 does.

Which is not much, on purpose. You should be able to read this page in one sitting.

Quick start

DevLink365 shares a server you are already running. Start your app first.

npm run dev          # terminal 1
npx devlink365       # terminal 2

That is it. The CLI prints a public HTTPS URL. Send it to someone; they see your app.

  ✓ DevLink365 is running

  Local:   http://localhost:3000
  Public:  https://a8f3k2qd.devlink365.dev

  Expires: in 4 hours

Nothing is installed globally — npx fetches the package, runs it, and forgets it. If you use it constantly:

npm install -g devlink365

Requires Node.js 18.17 or newer. The package has no runtime dependencies.

CLI reference

FlagWhat it doesDefault
-p, --port <number>Port your app is listening on.detected
--host <host>Local host to forward to.127.0.0.1
-e, --expires <time>How long the link stays live: 30m, 4h, 1h30m, 24h.4h
-n, --name <label>Prefix for the public subdomain.random
--relay <url>Relay to connect to. Point this at your own.wss://relay.devlink365.dev
--jsonPrint one JSON line with the tunnel details, then stay quiet.off
-q, --quietDo not log individual requests.off
--no-rewrite-hostPass the public Host header through to your app instead of rewriting it.off
--insecureSkip TLS verification. Self-hosted relays with self-signed certificates only.off
-y, --yesDo not ask for confirmation.off
-h, --helpShow help.
-v, --versionShow the version.

A bare number is treated as a port, so npx devlink365 5173 works too. The environment variable DEVLINK365_RELAY sets a default relay.

Choosing a port

With no --port, DevLink365 probes the ports that development tooling actually uses — 3000, 5173, 8080, 4321, 8000, 4200 and a dozen more — and also reads the dev script in a nearby package.json for a port you have configured yourself.

It only ever auto-selects a port that answered with a real HTTP response. A port that is open but does not speak HTTP is almost always a database, a language server, or something else that has no business being on the public internet, so it is never chosen for you. You can still share one deliberately with --port.

If more than one dev server is running, the most likely one is used and the others are printed so you can pick a different one.

Link lifetime

Every link dies. That is the point — a link you forgot about is a security problem, so DevLink365 does not let you make one.

npx devlink365 --expires 30m
npx devlink365 --expires 4h      # the default
npx devlink365 --expires 24h     # the maximum

The ceiling is 24 hours and it is enforced by the relay, not by the CLI, so it cannot be argued out of. You get a warning in the terminal at five minutes and again at one minute.

Three things end a link: the clock running out, the CLI exiting, and your machine losing its connection for long enough to fail a keepalive. In all three cases the URL stops resolving to your app straight away and visitors get a plain page explaining that the link has expired.

Naming a link

npx devlink365 --name checkout-demo
# https://checkout-demo-k7m2xq.devlink365.dev

--name is a prefix, not a reservation. Random characters are always appended, because a guessable name must never produce a guessable URL. Reserved labels — www, api, admin, login and similar — are refused outright and you get a random id instead.

Library API

The same tunnel, driven from code. Useful when a test suite or CI job needs a public URL for a few minutes.

import { createTunnel } from "devlink365";

const tunnel = await createTunnel({
  port: 3000,
  ttlMs: 60 * 60 * 1000,   // one hour
  label: "ci-preview",
});

console.log(tunnel.details?.url);

tunnel.on("request", ({ method, url, status, durationMs }) => {
  console.log(method, url, status, durationMs);
});

// when you are done
tunnel.stop();

The tunnel emits ready, request, expiring, notice, error and closed. Types ship with the package.

For scripts that only need the URL, --json is simpler:

URL=$(npx devlink365 --json | head -1 | node -pe 'JSON.parse(require("fs").readFileSync(0)).url')

Security

DevLink365 exposes a development server to the public internet. Here is the honest accounting.

What protects you. URLs come from a cryptographic random source and are not enumerable. Links expire, with a hard 24-hour ceiling enforced server-side. Closing the CLI kills the URL immediately. Only one level of subdomain is ever routed, so nothing slips past the wildcard certificate. Reserved hostnames can never be claimed. Request bodies are capped at 25 MB, visitors are rate limited per tunnel, and a single machine cannot open an unlimited number of links.

What does not protect you. There is no password, no visitor authentication, and no allow-list yet. Anyone who has the URL can use it for as long as it lives.

Treat a DevLink365 URL as public. Do not point one at an app holding production data, real credentials, or an admin interface you would not hand to a stranger.

Your local app sees X-Forwarded-Proto: https, X-Forwarded-Host with the public hostname, and X-Forwarded-For with the visitor's address. By default the Host header is rewritten to your local origin, which is what makes most frameworks behave as though nothing unusual is happening. Use --no-rewrite-host if your app needs to see the public hostname instead.

Self-hosting a relay

The relay ships in the same package, so you can run the whole thing yourself.

npx devlink365-server --domain tunnels.example.com --trust-proxy
npx devlink365 --relay wss://tunnels.example.com

You need two DNS records pointing at the relay and a wildcard certificate:

RecordTypePoints to
tunnels.example.comA / AAAA / CNAMEyour relay host
*.tunnels.example.comA / AAAA / CNAMEyour relay host

Terminate TLS in front of the process — Cloudflare, Caddy, or a load balancer are all fine — and make sure it forwards WebSocket upgrades. Start the relay with --trust-proxy so rate limits see the real visitor address rather than your proxy's.

Relay flagWhat it doesDefault
--domain <host>Base domain. Required.
--port <number>Port to listen on.8787 or $PORT
--scheme <https|http>Scheme used in the URLs handed to agents.https
--trust-proxyRead X-Forwarded-For. Only behind a proxy you control.off
--max-ttl <hours>Longest tunnel lifetime.24
--max-tunnels <n>Concurrent tunnels for the whole relay.5000
--max-per-ip <n>Concurrent tunnels per source address.5
--rpm <n>Requests per minute per visitor per tunnel.600

Health check: GET /_devlink/health on the apex returns JSON with the live tunnel count.

Troubleshooting

"Could not find a local web server to share"

Nothing was listening on any of the usual ports. Start your app first, then run DevLink365 in a second terminal. If your app is on an unusual port, name it: npx devlink365 --port 4321.

My app loads but the styles are missing

Usually an absolute asset URL pointing at localhost. Most dev servers handle this once they see X-Forwarded-Host; if yours needs the public hostname in Host, try --no-rewrite-host.

Hot reload keeps disconnecting

WebSockets are forwarded, but some dev servers hard-code the reload socket to localhost. Vite users can set server.hmr.clientPort: 443; Next.js generally works as-is.

The link stopped working before it expired

Almost always the machine sleeping or the network dropping. The CLI notices a failed keepalive and closes the tunnel rather than leaving a URL that silently swallows requests. Run it again for a fresh link.

"This relay is at capacity"

Too many tunnels are open. Wait a minute, or run your own relay — see self-hosting.