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:
npm install --save-dev @tnldotdev/tnl@next
npm exec -- tnl initThe 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:
npm exec -- tnl devEach 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:
npm exec -- tnl dev apiFor an ad hoc fixed-port process, the development command and arguments follow --:
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.
| priority | source |
|---|---|
| 1 | --host or --subdomain |
| 2 | TNL_HOST or TNL_SUBDOMAIN |
| 3 | named service host or subdomain |
| 4 | worktree 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:
tnl dev --ephemeralAn 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:
tnl dev --allow-ip 198.51.100.0/24tnl 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:
| state | meaning |
|---|---|
| starting | configuration, authentication, child startup |
| provisioning | route reconciliation, certificate, publisher connections |
| ready | route is accepting allowed visitors |
| draining | new work stopped while existing connections finish |
| stale | a 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:
tnl statusThe 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_TIMEOUTreports that the flow expired. - the framework did not connect: use
@tnldotdev/tnl/viteor@tnldotdev/tnl/next, or provide a fixed--port; followTNL_FRAMEWORK_REGISTRATION_TIMEOUTfor config checks. - the target did not listen: make the child honor
PORT, correct the service target, or increase the configured startup timeout.TNL_TARGET_MISMATCHmeans 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 statusto 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 currentandtnl route list;TNL_ROUTE_CONFLICTdoes not permit an ownership takeover. - visitors are denied: use the aggregate warning and current IP policy to decide whether to add a
narrow
--allow-ipvalue or intentionally use--public. - provisioning does not advance: keep the command running for retryable certificate or relay
work; use the
TNL_PROVISIONING_STALLEDhelp 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.