tnl.dev :: docs

development workflow.

run and reconcile worktree-aware named development services.

tnl dev is the project workflow. It resolves the selected team and one named service, starts its local development process, waits for the actual loopback target, and reconciles the corresponding route. When the child or tunnel stops, tnl shuts down the other side cleanly.

initialize once

Install the single package and initialize the project:

initialize a development workflow
npm install --save-dev @tnldotdev/tnl@next
npm exec -- tnl init

The initializer detects supported frameworks and package scripts, then creates a reviewable project configuration and a missing framework config. It preserves existing Next.js, Vite, and TypeScript configuration and reports the exact integration actions still needed. Add further named services in the project config. See project configuration for manual TypeScript, YAML, and JSON forms.

Before starting a configured service, tnl dev regenerates .tnl/project.json and .tnl/project.d.ts from the selected team, member namespace, and exact service routes. Framework integrations consume these files; they are local generated state and should be ignored by Git.

run named services

If the project has one configured service, start it without an argument:

start public development
npm exec -- tnl dev

Each configured name identifies one local service and one route in output. Projects can keep a web application, API, and callback receiver together without relying on port numbers as identities. A project with multiple services requires the service name:

start the API service
npm exec -- tnl dev api

For an ad hoc fixed-port process, the development command and arguments follow --:

run an ad hoc fixed-port service
tnl dev --port 3000 -- npm run dev

--port sets PORT for the child and requires that exact loopback port. Without --port, the Vite or Next.js integration preserves the framework's default or configured port behavior and reports the actual target after the server binds. If an occupied preferred port causes a retry, tnl uses the new target instead of tunneling a different process on the old port. Framework host defaults and custom host settings are also preserved. A user host setting can independently expose the service on the LAN; it is not required for tnl and tnl does not add LAN exposure.

automatic authentication

If the selected server has no usable saved session, tnl dev begins browser authentication and opens the verification URL. Complete sign-in once; the pending command resumes automatically. When browser launch is unavailable, tnl prints the exact URL and leaves the same flow available for manual opening. Interactive login lasts at most ten minutes and can expire sooner when the provider sets a shorter device-code lifetime. An expired flow reports a new URL for the next attempt; user cancellation remains cancellation rather than a timeout.

The child process never receives the saved login or TNL_ACCESS_TOKEN. Framework integrations receive only a private bootstrap socket and public route metadata.

worktree hostnames

The default hostname combines the service name, a readable worktree name, and an eight-character hash. The hash is stable for one worktree and client state directory, so repeated runs reuse the same route. Other worktrees and installations get different hostnames. Outside Git, tnl uses the project directory. The hash's private random input is never included in the URL or exposed to project configuration.

hostname inputs: The ordered sources tnl uses to select a service hostname.
prioritysource
1--host or --subdomain
2TNL_HOST or TNL_SUBDOMAIN
3named service host or subdomain
4worktree label plus service name

All forms remain constrained by the selected team, domain, route scope, and membership namespace. An explicit value changes selection, not authorization.

route reconciliation

Before creating a route session, tnl compares the desired hostname, target, and IP policy with the durable route. It creates a missing route, updates an owned route whose mutable settings differ, or reuses a matching route. It never silently takes over a hostname owned by another route or team.

This makes project configuration declarative: changing a service's target or visitor policy is applied on the next run instead of producing a generic conflict. Only one active route session can publish a route at a time.

Routes persist after normal shutdown by default. That preserves ownership and certificate reuse between runs. For disposable previews, request an ephemeral route:

delete routes after the run
tnl dev --ephemeral

An ephemeral route is deleted during clean shutdown. If the process cannot complete cleanup, tnl status and tnl route list show the remaining state so it can be reconciled or deleted later.

visitor policy

The default IP policy permits the current public IP observed by the control API. Repeat --allow-ip for additional addresses or prefixes, or use --public to allow all visitor addresses:

allow a review network
tnl dev --allow-ip 198.51.100.0/24
allow all visitor addresses
tnl dev --public

--public and --allow-ip are mutually exclusive. Public development servers need their own application authentication and must not assume every request is trusted.

Ingress rejects disallowed visitor connections before forwarding. The publisher reports those denials as bounded aggregate warnings with a count and interval, not one warning per connection and not a stream of visitor addresses.

provisioning states

Every service advances through deterministic lifecycle states:

development states: Stable states shown by tnl dev and tnl status.
statemeaning
startingconfiguration, authentication, child startup
provisioningroute reconciliation, certificate, publisher connections
readyroute is accepting allowed visitors
drainingnew work stopped while existing connections finish
stalea previous local record no longer has a live publisher

Provisioning output changes only when the known state changes. Certificate or relay retries stay in the same state rather than resetting an artificial percentage. If one route version remains in provisioning for two minutes, tnl emits one TNL_PROVISIONING_STALLED warning and keeps retrying. The warning is canceled when that version becomes ready, is replaced, or the tunnel ends.

Publisher connections prefer QUIC. If QUIC fails, or has not completed its authenticated tunnel handshake after 250 milliseconds, tnl starts TLS/TCP and uses the first authenticated transport. A TLS/TCP winner produces one transport fallback warning per route version while development continues normally. The warning does not claim QUIC is unavailable; it reports which transport won.

contextual status and help

Use another terminal while development is running:

inspect this project
tnl status

The default view is scoped to the current worktree project and includes service, hostname, target, route version, and lifecycle state when available. tnl status --all shows every local tunnel and adds its worktree project root. Use tnl status --output=json for automation.

Errors are rendered once with the command that failed. Known diagnostics include a stable code and contextual https://tnl.dev/e/... URL. Follow that URL before changing unrelated configuration; it corresponds to the exact failed boundary.

troubleshooting

  • authentication did not finish: open the printed URL, complete the pending browser flow, and rerun if TNL_AUTHENTICATION_TIMEOUT reports that the flow expired.
  • the framework did not connect: use @tnldotdev/tnl/vite or @tnldotdev/tnl/next, or provide a fixed --port; follow TNL_FRAMEWORK_REGISTRATION_TIMEOUT for config checks.
  • the target did not listen: make the child honor PORT, correct the service target, or increase the configured startup timeout. TNL_TARGET_MISMATCH means a forced port was not honored.
  • the service is ambiguous: select one of the sorted names in TNL_SERVICE_AMBIGUOUS.
  • the hostname is already active: use tnl status to find the publishing worktree and stop that tunnel before retrying.
  • the route cannot be reconciled: confirm the selected team and route ownership with tnl team current and tnl route list; TNL_ROUTE_CONFLICT does not permit an ownership takeover.
  • visitors are denied: use the aggregate warning and current IP policy to decide whether to add a narrow --allow-ip value or intentionally use --public.
  • provisioning does not advance: keep the command running for retryable certificate or relay work; use the TNL_PROVISIONING_STALLED help URL to check the delayed boundary.
  • transport fallback: tnl is continuing over TLS/TCP because QUIC did not establish first; a self-hosted operator should verify public UDP reachability when the warning is persistent.