tnl.dev :: docs

publish a service.

publish an existing loopback service and control the route lifecycle.

tnl publish attaches an already-running loopback HTTP service to a route. Use it when tnl should not start or supervise the local process; use tnl dev for the initialized project workflow.

publish a target

Pass a port when the local service listens on loopback:

publish port 3000
tnl publish 3000

You can also pass a loopback HTTP origin. Paths, query strings, credentials, HTTPS targets, and non-loopback addresses are rejected.

local targets: Accepted target forms and the loopback HTTP origin they select.
inputtarget
3000http://127.0.0.1:3000
localhost:3000http://127.0.0.1:3000
http://127.0.0.1:3000http://127.0.0.1:3000
http://[::1]:3000http://[::1]:3000

tnl checks the target before creating a route session. Start the local service first and keep the publisher running while the URL is needed.

Pass a configured service name instead of a target to apply that service's target, team, tunnel, and root project defaults:

publish a configured service
tnl publish web

With one configured service, omitting the argument selects it. With multiple configured services, tnl reports TNL_SERVICE_AMBIGUOUS with sorted names instead of guessing.

select the team and hostname

The route belongs to the selected team. A named service team overrides the root project team, which overrides the team saved by tnl team use. Check the effective choice before publishing an important hostname:

show the selected team
tnl team current

tnl team current shows the stored fallback. Run tnl config check to validate a root or named service team override before the tunnel starts.

With no hostname option, tnl derives a hostname from the current Git worktree and named service, then places it in your member namespace on the team's default domain. Outside Git, it uses the project directory. Repeating the command from the same worktree and client state selects the same durable route; another installation derives another hostname.

Choose a stable child label with --subdomain, or an exact authorized hostname with --host:

publish an explicit member route
tnl publish 3000 --subdomain api
publish an exact hostname
tnl publish 3000 --host api.dev.example.com
hostname selection: Explicit command values take precedence over environment, project configuration, and the derived default.
inputresult
--host <hostname>exact authorized hostname
--subdomain apiapi beneath the member namespace
service configurationconfigured host or subdomain
no explicit valueworktree and service derived hostname

--host and --subdomain are mutually exclusive. Explicit hostnames still obey team, domain, membership namespace, and route-scope authorization. Admins and owners can manage shared routes but cannot publish inside another member's namespace.

control visitor access

Routes are safe for the current network by default. The control API observes the publisher's public IP and tnl includes it in the route's IP policy.

Repeat --allow-ip to add visitor addresses or CIDR prefixes:

allow additional visitor networks
tnl publish 3000 \
  --allow-ip 198.51.100.24 \
  --allow-ip 2001:db8:1234::/48

To accept visitors from every address, use --public:

publish a public route
tnl publish 3000 --public

--public and --allow-ip are mutually exclusive. Public routes require application-level authentication when the service is not intended for everyone.

Ingress denies disallowed connections before forwarding them. The publisher emits bounded, aggregate denied-visitor warnings with counts over an interval; it does not log one line or source address per denied connection. A warning usually means a visitor is outside the current-IP policy, not that route TLS or the target failed.

reconcile the route

tnl treats the selected hostname, target, and IP policy as desired state. Before opening a route session, it:

  1. creates the route if it does not exist,
  2. reuses it when the mutable settings already match,
  3. updates an owned route when the target or IP policy changed, or
  4. fails without taking over when ownership or immutable identity does not match.

Reconciliation means a config change does not require deleting the route just to change its target or visitor policy. The update remains authorized against the route's team and route owner. A hostname ownership, immutable identity, lifecycle, or concurrent reconciliation conflict reports TNL_ROUTE_CONFLICT without taking over or deleting the route.

Only one active route session can publish a route. A new continuous run receives a new route version; changing mutable desired state does not transfer route ownership.

choose persistent or ephemeral

Routes are persistent by default. Pressing Ctrl+C ends the route session and publisher connections, but the durable route remains for the next reconciliation:

inspect persistent routes
tnl route list

Use --ephemeral for a disposable preview whose route should be deleted after clean shutdown:

publish an ephemeral route
tnl publish 3000 --ephemeral

Ephemeral affects the durable route, not just the local process. Do not use it for a hostname whose ownership or certificate reuse should survive between runs. If cleanup is interrupted, inspect the route and delete it explicitly when appropriate:

delete a remaining route
tnl route delete route_0123456789abcdef0123456789abcdef

observe lifecycle and failures

Add --open to open the URL after the route becomes ready:

publish and open the route
tnl publish 3000 --open

The publisher reports deterministic lifecycle states such as starting, provisioning, ready, and draining. Retryable certificate or relay work remains in provisioning rather than resetting an estimated progress indicator. After two minutes, one TNL_PROVISIONING_STALLED warning is emitted for that route version while retries continue. Readiness, a replacement route version, or tunnel shutdown cancels the pending warning.

Each publisher connection starts with QUIC and starts TLS/TCP if QUIC fails or has not completed its authenticated tunnel handshake after 250 milliseconds. The first authenticated transport wins; a terminal authentication or stale-assignment rejection stops both attempts. If TLS/TCP wins, tnl emits one transport fallback warning for the route version and continues normally. The warning means QUIC did not establish first, not necessarily that QUIC is unavailable.

Inspect local tunnels from another terminal:

show contextual status
tnl status

Status is scoped to the current worktree project and includes the service, hostname, target, route version, and current lifecycle state when available. Use tnl status --all to include every local tunnel and its worktree project root. Known failures include a stable diagnostic code and contextual help URL for the failed boundary.

Automation can consume newline-delimited lifecycle events:

stream machine-readable events
tnl publish 3000 --output=ndjson

Human lifecycle output goes to stderr; finite status and machine-readable results go to stdout. In NDJSON, provisioning stalls are structured non-error warning events with code, help_url, route_version, and retryable: true. Transport fallback is a warning event with transport: "tls-tcp", route_version, and retryable: false. tnl dev preserves child output and therefore has no NDJSON lifecycle mode.

Continue with project configuration for named service defaults or teams and domains for ownership and namespace rules.