tnl gives your local app an end-to-end encrypted HTTPS URL. By default, every
Git worktree gets its own unique URL, so you can easily preview multiple
development tracks in parallel.
browser
|
HTTPS
v
tnld (hosted or self-hosted)
|
encrypted traffic
|
+-----------------------------v-----------------------------+
| your machine |
| |
| main repo perf worktree |
| web URL -> :5173 web URL -> :5174 |
| api URL -> :3001 api URL -> :3002 |
| |
+-----------------------------------------------------------+
First, install the NPM package:
npm install -D @tnldotdev/tnl@nextThen, initialize with the helper:
npx tnl inittnl init walks you through creating a tnl.config.ts with the command
that starts your app. For example, in a Vite project:
// tnl.config.ts
import { defineConfig } from "@tnldotdev/tnl/config";
export default defineConfig({
services: {
app: {
directory: ".",
dev: { command: ["vite", "dev"] },
},
},
});Then run:
npx tnl devSign in to GitHub when prompted. Your app will start on the next available port,
and tnl publishes to its unique URL, with only your current IP whitelisted:
+--[ tnl dev ]-- ready ----------------------------------------+
| |
| https://app-example-abcd1234.ecstatic-penguin.tnl.dev |
| | |
| v |
| tnl |
| | |
| v |
| http://127.0.0.1:5173 |
| |
| framework vite |
| automatically allowed IP 122.151.3.101 |
| |
+-- ctrl+c to stop --------------------------------------------+
Hot reloading works automatically, as well as SSE/websockets/streaming responses.
You can allow reviewers or webhooks when
you need them. For example, tnl dev --allow-provider stripe allows Stripe's
published webhook IPs alongside your current IP.
Follow the quickstart or read about framework setup.
If your project has multiple separate services, you can put both startup commands
in tnl.config.ts:
// tnl.config.ts
import { defineConfig } from "@tnldotdev/tnl/config";
export default defineConfig({
services: {
web: {
directory: "apps/web",
dev: { command: ["vite", "dev"] },
},
api: {
directory: "apps/api",
dev: { command: ["pnpm", "dev"] },
},
},
});Start both services either together or individually:
npx tnl dev
# or
npx tnl dev web
npx tnl dev apiThese will have separate URLs:
| service | URL |
|---|---|
web |
https://web-example-k7n2p9.ecstatic-penguin.tnl.dev |
api |
https://api-example-k7n2p9.ecstatic-penguin.tnl.dev |
Use the API URL assigned to this project in the frontend:
import { tnl } from "@tnldotdev/tnl";
const apiURL = tnl.services?.api?.url;
// https://api-example-k7n2p9.ecstatic-penguin.tnl.devRead about project configuration and
framework metadata.
For a Node or Bun API, use tnl.port and tnl.register(server) so worktrees
can listen on different ports. See API server setup.
Create another Git worktree. The project configuration comes with it:
git worktree add -b perf ../perfThen start the two services:
npx tnl dev
# or
npx tnl dev web
npx tnl dev apiThe same frontend is now running from two checkouts:
| checkout | frontend public URL |
|---|---|
| primary | https://web-example-k7n2p9.ecstatic-penguin.tnl.dev |
../perf |
https://web-example-perf-m4q8s2.ecstatic-penguin.tnl.dev |
tnl names each URL after the service and project, adding the directory name
for a linked worktree. A short ID keeps names distinct, and switching branches
leaves the URL unchanged.
For Next.js and Vite, there is no port bookkeeping. If another worktree already
uses the preferred port, the framework can choose another and tnl follows the
listener it actually opens. Restarting the same worktree reuses its URL.
See what is running across your local worktrees from any of them:
npx tnl status --allClaim a development subdomain and make it the default for new public URLs:
npx tnl domain claim dev.example.com --default
npx tnl domain listDelegate the subdomain with the NS records. Then, dev URLs will look like:
https://web-example-k7n2p9.chase.dev.example.com
After deploying the tnl server, update your config with it:
export default defineConfig({
server: "https://control.example.com",
services: {
// ...
},
});The same config works with hosted tnl.dev or your own server.
You can use Homebrew to install the tnl client. Publish an HTTP app
already listening on port 3000 without any configuration:
brew install tnldotdev/tap/tnl
tnl publish 3000This works very similarly to tnl dev. Read about tnl publish.
tnl.dev is free for now. Teams have a soft limit of 50 GiB of transfer per month across their public URLs, counting traffic in both directions.
Accounts and teams created while it is free will keep a free plan if paid options arrive.
The client and server are MIT-licensed. See the dependency notices, browse the command reference, or contribute.