HTTPS names for dev environments that aren’t your laptop.

Run one daemon on the machine with the browser. Each container, VM, WSL instance, or remote box registers a stable name over a single TCP connection.

testd run app.example:3000 -- npm run dev then open https://app.example.test
Cross-section of a testd host, drawn as a volcano The host is a cone of three layers around a core: DNS for star dot test, the HTTPS edge, the reverse proxy, and a core holding the local CA and tokens. Below an erosion surface that stands for the network, four dev environments sit in the basement as stones: a VM, a container, a WSL instance and a remote box. An ember line runs from each one up to the host's intake on port 7737 and on to the vent, and four names are thrown out of the vent: app, api, admin and shop dot example dot test. :7737intake VM testd run app.example:3000 container testd run api.example:8080 WSL testd run admin.example:5173 remote box testd run shop.example:4000 DnDNS for *.test EdHTTPS edge PxReverse proxy AuLocal CA and tokens one TCP connection per leaf app.example.test api.example.test admin.example.test shop.example.test
Section through a testd host. Not to scale.

The host is the authority.

The browser never connects to the leaf. It resolves the name to the host, the host terminates TLS, and the host proxies to the target.

No Caddy, no per-project /etc/hosts edits, and no per-project certificates.

  • DNS for *.test

    A built-in server on loopback answers each registered name with the host’s address, not the app’s.

    127.0.0.1:7753
    :53 on Windows
  • HTTPS edge

    Terminates browser TLS with rustls. Leaf certificates are minted on demand for registered names and live 24 hours.

    0.0.0.0:443
  • Reverse proxy

    Speaks HTTP/1.1 and handles WebSocket upgrades, so HMR connections work. The app receives the original Host header.

    plain HTTP
    to the app
  • Local authority

    The local CA, the operator secret, and the issued tokens live on the host. The root key is never copied to a dev environment.

    ~/.config/testd
  • The connection

    A leaf keeps one outbound TCP connection open and registers names on it. A crash or lost network releases them without a cleanup job.

    :7737
    NDJSON
  • Leaf

    The VM, container, WSL distro, or remote box that runs testd run and serves the app.

    testd run

Three commands.

Two on the host, one in the project. The host appends .test, so app.example opens at https://app.example.test.

  1. testd init

    Host setup, once. Creates the config, a local CA, and an operator secret. Run testd doctor --fix to configure the .test resolver and install root trust.

  2. testd host

    Runs the daemon. It owns DNS, the HTTPS edge, and the reverse proxy. The tray app can run it for you.

  3. testd run :3000

    Registers the current directory, with the name inferred from it. Keep it running: closing the terminal removes the name.

Name it yourself, wrap a process, or publish several names on one connection.

testd run app.example:3000
An explicit name
testd run app.example:3000 -- npm run dev
The name lives as long as the process
testd run frontend:3000 api:8080 -- npm run dev
Two names, one connection
testd run 10.0.0.5:myapp:3000 -- npm run dev
A custom target host

Two labels, so cookies behave.

app.example.test and api.example.test share a registrable domain, so cookies behave like they do on app.example.com and api.example.com.

A single-label app.test works, but it is cross-site with every other project. testd prints a warning when you use one.

Labels take ASCII letters, digits, and hyphens. The name testd.test is reserved for the dashboard.

The connection is the lease.

A registration lives exactly as long as the TCP connection that created it. Permission lasts longer: a token is the key, its grants are the doors it may open, and a claim is a door currently held open.

Try an event

https://app.example.test

Tokenthe key
Kept
Grantthe doors it may open
Kept
Claima door held open
Live on this connection
Leaf
Connected, and serving the name

Access by request, one name at a time.

A token covers exact names, not a project wildcard or everything a machine can reach.

  1. On the leaf testd run --host 192.168.1.5 :3000

    With no credential for this profile, the leaf asks the host for a grant and names the domains it wants.

  2. On the host testd requests testd approve <id>

    A request waits up to two minutes. The dashboard’s Access card and the tray notification make the same decision.

  3. On the leaf ~/.config/testd/credentials/default

    The token is saved here with mode 0600, so later runs do not ask again.

  4. On the host testd tokens --revoke <id>

    Publishing a new name raises its own request. Revoking a token closes its connections and releases its names.

Every live name, on one local page.

testd dashboard prints an authenticated URL. The page lists live names, shares, access requests, issued tokens, quick launches, and root trust, and updates without a reload.

It is served only on loopback, and the edge does not proxy it. A bare URL is read-only.

Tray app

Runs the daemon and opens the dashboard. On first start, a wizard sets up the DNS resolver, the root certificate, and the operator secret. It posts a notification while an access request waits.

Quick launches

A saved command, started from the dashboard or the tray. Each name moves through configured, probing, and confirmed, and is registered only after the target accepts.

The testd dashboard in its light theme, listing four active domains, a pending access request, an issued token, a running quick launch, a cloudflared share, and a trusted root certificate. The testd dashboard in its dark theme, listing four active domains, a pending access request, an issued token, a running quick launch, a cloudflared share, and a trusted root certificate.
The dashboard page from this repository, rendered with sample data.

When a name fails, the exit code names the layer.

testd doctor
testd doctor --json
testd doctor --fix

--fix configures the resolver, writes a missing secret, and installs or reinstalls the root. While the daemon runs, it re-checks the resolver every 10 seconds and root trust every 60. On a desktop, activating the notification runs the repair.

0
Ready
1
Configuration or missing state
2
Resolver or name resolution
3
PKI, certificate trust, or the HTTPS edge

Four paths, and what protects each one.

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

PathTransportTrust
Browser to host edgeTLS, rustls with the ring providerThe browser trusts the testd root or a sharing provider
Leaf to host intakePlain TCP, NDJSONBearer credential in the clear. The network must be trusted or separately encrypted
Host to app targetPlain HTTP/1.1The app should not assume the upstream hop is encrypted
Dashboard and control channelLoopback TCP, no TLSSame-user local access plus a per-run token

Certificate lifetimes

  • 10 yearsRoot CA, the trust anchor in the system store
  • 1 yearIntermediate, renews near expiry without changing root trust
  • 24 hoursLeaf, minted on demand for a name

Bars on a logarithmic scale, starting at one hour.

Bounded work

The daemon bounds work so a buggy or hostile local client cannot exhaust it without effort. These are robustness limits, not authentication.

256
simultaneous intake connections
512
edge connections, 64 per source address
64 KiB
NDJSON frame
32 KiB
HTTP request head
256 KiB
HTTP request body
32
pending access requests, two minutes each
1024
cached edge certificates

Report a vulnerability

Use GitHub private vulnerability reporting, not a public issue. Expect an acknowledgement within 7 days and a fix or mitigation plan within 30 days.

What testd does not do

  • No telemetry, usage data, or name reporting
  • No public DNS or ACME dependency
  • No vendor SDK for tunnels
  • No remote administration API
  • No guarantee of isolation between projects that share a host or a token profile
  • No name that outlives its leaf connection or target

Install

0.1.0-alpha is the first public release, from 3 November 2026. The CLI, daemon, and tray ship as one version.

Desktop bundles are unsigned and installed by hand. Verify SHA256SUMS before installing.

From source, with Rust 1.89, or 1.90 for the tray

git clone https://github.com/LapilliWorks/testd
cd testd
cargo install --path .
macOS .tar.gz CLI, .dmg tray, generated Homebrew definitions. Apple Silicon and Intel.
Linux .tar.gz CLI, .AppImage and .deb tray, x86-64. Port 443 needs CAP_NET_BIND_SERVICE or a high edge_addr.
Windows .zip CLI, per-user NSIS installer, x86-64. DNS uses UDP 53, and NRPT rules route .test.

Leave when you like

testd uninstall
testd uninstall --purge

Stop the daemon first. uninstall removes the OS resolver route and the trusted root. --purge also deletes ~/.config/testd. Run it before removing the tray app, because the OS uninstaller leaves both in place.

No in-app updater and no account. The binaries never check for a new version.