tnl.dev :: docs

how tnl works.

Follow a public URL from a visitor to your local app.

from your browser to your app

tnl gives a local HTTP app a public HTTPS URL. A visitor reaches the tnl server first, which carries the connection to the tnl process on your machine. The server does not need to run your app or decrypt its HTTP traffic. Visitor TLS ends in local tnl; only then does tnl send HTTP to your app over localhost.

the parts of a tnl server

The tnl server has three roles. Control keeps public URL and publish run state in PostgreSQL, authorizes changes, chooses relay services, and coordinates certificates. Ingress accepts visitor connections and chooses a connected relay using a short-lived routing table from control. Relays hold publisher connections from local tnl processes and carry visitor streams. Only control uses PostgreSQL.

Public URL DNS points to ingress, not to the selected relay. Control can replace a connection assignment without changing where visitors look up the hostname.

public urls and publish runs

A public URL saves a hostname, local target, visitor policy, and state. A publish run is one active publication of it. Each tnl dev or tnl publish tunnel creates a new run with the next publish run number for that public URL. Stopping a normal tunnel ends the run and leaves the public URL saved; visitors cannot reach your app until another run is ready. An ephemeral public URL is removed when its tunnel stops.

Control approves a certificate plan: which hostnames a public URL certificate may cover and how to prove control of them. Local tnl keeps the visitor TLS private key and obtains the certificate through control's issuance flow. The certificate is installed in tnl, where visitors actually complete the TLS handshake.

Each run has two publisher connection slots. Control assigns them to different relay services, and local tnl connects to both:

For each connection, tnl tries QUIC first. After a short delay it also tries TLS/TCP and keeps the first authenticated transport that completes. This transport is separate from visitor TLS. A run first becomes routable when its public URL certificate and both publisher connections are ready. After that, one ready connection can keep serving while tnl replaces the other.

If an established QUIC publisher connection fails, tnl waits for control's replacement assignment and tries TLS/TCP first for that slot, with QUIC as a second candidate. A later replacement returns to QUIC-first after TCP recovery. Visitor connections already carried by the failed transport close; they cannot resume on TCP.

follow a visitor connection

Ingress reads the visitor's TLS ClientHello to find the requested hostname, looks up the active publish run, and checks its IP policy. It chooses one ready connected relay. Ingress adds the original source and destination as PROXY v2 metadata once, then forwards the connection to that relay over an authenticated internal connection. The relay opens a visitor stream on its existing publisher connection to local tnl. Within a separate small capacity budget, denied visitors can complete TLS at tnl and receive a 403; their requests never reach the local service.

The relay preserves the metadata and visitor TLS bytes. Local tnl checks the hostname, presents the public URL certificate, and terminates visitor TLS. It validates the HTTP request, removes client-supplied forwarding headers, and adds its own before proxying to the local service. Responses travel back through that visitor stream and ingress.

what happens when a relay stops

If stream setup fails, ingress can try another connected relay before it sends the first visitor byte. That first byte is the retry boundary. Once it has crossed, a failure closes the visitor connection. An in-progress connection is not replayed or moved to another relay, even if the local app never received a full request.

New visitor connections can use the remaining ready publisher connection while tnl replaces the lost one. If both connections are unavailable, there is no path to the local app until a connection returns.

hosted and self-hosted layouts

Hosted and self-hosted servers use the same roles. The difference is who runs them and which failures they share:

In standalone, two logical relay services share one tnld process and its failure boundary; PostgreSQL remains external. In a split deployment, control, ingress, and relays run in separate tnld processes. The checked-in split Compose setup puts both relay containers on one host, so it separates processes but not host failures. Place relay services on separate hosts or other failure boundaries if you need them to survive the loss of one. See self-hosting for deployment choices.