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.
$ testd init ✓ config.toml · local CA · operator secret $ testd doctor --fix ✓ resolver route + root trust installed $ testd host ✓ intake :7737 · edge :443 · dns :7753 $ testd run app.example:3000 -- npm run dev ✓ app.example registered → https://app.example.test
Ctrl+C removes the name. A registration lives only as long as the connection that created it.
Three strata, one small stone.
The host is the authority. The leaf holds one connection. The connection is the lease.
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
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
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
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.
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.
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.
Install the CLI
From source, or grab a release archive and verify the checksums before installing.
cargo install --path .
# or: release archives + SHA256SUMS
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
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