testd

Stable HTTPS names for everything that runs somewhere else.

testd gives a project in a container, VM, WSL distro, or remote box a stable name on the machine with the browser. The host owns the DNS, the local CA, and the reverse proxy; each dev environment registers over a single TCP connection. No Caddy, no /etc/hosts edits, no per-project certificates.

Ctrl+C removes the name. A registration lives only as long as the connection that created it.

01 · How it works

Three strata, one small stone.

The host is the authority. The leaf holds one connection. The connection is the lease.

STRATUM I

The host is the authority

The machine with the browser runs testd host. It answers DNS for *.test, terminates TLS with leaf certificates minted by its own local CA, and proxies each live name to the dev environment's port. The root key never leaves the host.

testd host
# dns :7753 · edge :443 · intake :7737
STRATUM II

The connection is the lease

Each dev environment keeps one outbound TCP connection open and registers its names on it. A crash, a Ctrl+C, or a lost network releases the name; no cleanup job, no stale entries. The leaf reconnects every few seconds and publishes the same names again.

testd run :3000
# the name lives as long as the command
STRATUM III

Names shaped like real domains

app.example becomes app.example.test: two labels, so cookies behave like they do on app.example.com and api.example.com. A single-label name works, but testd warns about it.

app.example.test
api.example.test
BROWSER https://app.example.test HOST MACHINE the one with the browser DNS *.test resolves names to the host HTTPS edge :443 leaf certs, minted on demand reverse proxy HTTP/1.1 · WebSockets DEV ENVIRONMENT container · VM · WSL · remote box $ testd run app.example:3000 1 the name resolves to the host, and TLS ends at its edge 2 one outbound TCP connection · NDJSON + bearer token close it and the name is gone 3 proxied to the app's port
02 · What you get

Small tool, sharp edges only where they belong.

Everything runs on your machines. There is no account, no telemetry, and no public DNS dependency.

Names with real cookie shape

<service>.<project> on the .test TLD gives app and api the same registrable domain they would have in production, so cookies, OAuth redirects, and CORS behave the same.

Access by request

A leaf with no credential asks the host and names the domains it wants. testd approve issues a token that covers exactly those names; revoking it closes the connections and releases them. The tray raises a notification while a request waits.

A loopback dashboard

Live names, shares, access requests, tokens, quick launches, and certificate controls, on 127.0.0.1:7737 with a per-run token. A bare URL is read-only; mutations need the token. Not proxied by the edge, ever.

Local CA, minted leaves

Leaf certificates are minted on demand for live names and live 24 hours. The root key stays on the host. The intermediate renews without changing root trust, and the previous key is kept under pki/backups/.

Doctor, tray, quick launches

testd doctor --fix repairs the resolver, trust, and Caddy wiring; exit codes are stable for scripts. The tray runs the daemon, lists launches, and posts a notification when a repair is needed.

Wrap a process, own a name

testd run frontend:3000 api:8080 -- npm run dev publishes both names for the lifetime of the process. HMR and WebSockets pass through; the app sees its real Host header.

03 · Sharing a name

Names live on your machines. Sometimes they need to travel.

Names resolve only on the host and the devices that trust its root. To reach one from a phone, a webhook, or a colleague, send it through a tunnel. The daemon owns the tunnel, so closing the terminal does not close the share; when the name goes, the tunnel goes too.

04 · Security model

Trust has boundaries. Here are all of them.

testd is a development tool. It gives names and HTTPS to machines you control, and it assumes the host is the trust authority. Not a production ingress, not a public DNS service.

Path Transport What it means
browser → host edge TLS, rustls The browser trusts the local root or a sharing provider. Certificates are minted on demand.
leaf → host intake plain TCP, NDJSON A bearer credential in the clear. The network must be trusted or separately encrypted.
host → app target HTTP/1.1 The app should not assume the upstream hop is encrypted.
dashboard + control loopback only No TLS, no login; a per-run token is the only authority, and a loopback Host header is enforced.
!

Do not expose port 7737 to the public internet. Prefer issued tokens over the operator secret, and reserve the secret for unattended bootstrap. A remote leaf may only target its own address unless you explicitly allow otherwise.

05 · Install

From a clean machine to a working name.

Release archives, SHA256SUMS, generated Homebrew definitions, and a separate tray build. No in-app updater, no commercial service.

Step 1

Install the CLI

From source, or grab a release archive and verify the checksums before installing.

cargo install --path .
# or: release archives + SHA256SUMS
Step 2

Prepare the host

init is idempotent: config, local CA, operator secret. doctor --fix configures the .test resolver and installs root trust.

testd init
testd doctor --fix
Step 3

Start the daemon, run a name

In the project, on the same machine or a remote one. Then just open the URL.

testd host
testd run app.example:3000 \
	-- npm run dev
# → https://app.example.test
LicenseISC
Accountsnone
Telemetrynone
Public DNSnone
ACMEnone
RuntimeRust