tnl.dev :: docs
project configuration.
configure a selected team and named services with TypeScript, YAML, or JSON.
Project configuration records the selected team and named local services beside the application.
Use TypeScript for worktree-aware values, or use YAML or JSON for a static document. tnl init
creates the initial file; the formats below are equivalent ways to maintain it.
install the configuration package
The client, configuration types, and framework integrations ship together:
npm install --save-dev @tnldotdev/tnl@nextImport framework integrations from @tnldotdev/tnl/vite and @tnldotdev/tnl/next.
configure with typescript
TypeScript configuration is implicitly format version 1 and uses camel-case field names:
export default {
team: "Acme Engineering",
services: {
web: {
directory: "apps/web",
publish: { target: 3000 },
dev: {
command: ["npm", "run", "dev"],
startupTimeout: "90s",
},
},
api: {
directory: "apps/api",
tunnel: { public: true },
publish: { target: 8787 },
dev: { command: ["npm", "run", "dev:api"] },
},
},
};The team value may be a team ID or an unambiguous display name. Service names are stable local
identifiers used in hostname derivation, reconciliation, status, and diagnostics. A service name is
one lowercase DNS label, begins with a letter, and is at most 32 characters.
server, team, tunnel, publish, and dev may also appear at the root of the tnl value as
project defaults. A named service inherits those defaults and overrides only the fields it supplies.
This keeps shared team and visitor policy in one place while each service owns its target and
command.
Use a factory only when a value truly needs project context:
export default ({ env, worktree }) => ({
team: env.TEAM_NAME ?? "Acme Engineering",
services: {
web: {
tunnel: { subdomain: `web-${worktree.label}` },
publish: { target: 3000 },
dev: { command: ["npm", "run", "dev"] },
},
},
});The factory receives a deeply frozen context:
| value | meaning |
|---|---|
| cwd | directory where tnl was invoked |
| env | environment without TNL_* or TNLD_* |
| worktree.root | Git worktree root, or cwd outside Git |
| worktree.name | base name of the root |
| worktree.label | DNS-safe name stable for the worktree and client state |
| worktree.isGit | whether a Git worktree was found |
Node.js 22.18 or newer is required to evaluate tnl.config.ts. The sanitized context prevents config code
from reading tnl client or server credentials through env; a project config is still trusted local
code with normal Node.js access.
configure with yaml
Static configuration always requires both the root version: 1 field and the root tnl: envelope.
It uses snake-case field names:
$schema: https://tnl.dev/schema/v1.json
version: 1
tnl:
team: Acme Engineering
services:
web:
directory: apps/web
publish:
target: 3000
dev:
command: [npm, run, dev]
startup_timeout: 90s
api:
directory: apps/api
tunnel:
public: true
publish:
target: 8787
dev:
command: [npm, run, dev:api]Do not omit version, move it under tnl, or move service fields beside the tnl envelope. Those
are different document shapes and strict validation rejects them.
configure with json
JSON has the same versioned root document and snake-case names:
{
"$schema": "https://tnl.dev/schema/v1.json",
"version": 1,
"tnl": {
"team": "Acme Engineering",
"services": {
"web": {
"directory": "apps/web",
"publish": { "target": 3000 },
"dev": {
"command": ["npm", "run", "dev"],
"startup_timeout": "90s"
}
},
"api": {
"directory": "apps/api",
"tunnel": { "allow_ip": ["198.51.100.0/24"] },
"publish": { "target": "http://127.0.0.1:8787" },
"dev": { "command": ["npm", "run", "dev:api"] }
}
}
}
}Do not place access tokens or other secrets in project configuration. Automatic browser authentication and saved client state supply credentials independently.
understand service fields
Each named service has the same route, publish, and development concerns:
| TypeScript | YAML / JSON | meaning |
|---|---|---|
| team | team | selected team ID or display name |
| services | services | map of stable service names |
| services.<name>.directory | services.<name>.directory | service directory relative to the config |
| tunnel.host | tunnel.host | exact route hostname |
| tunnel.subdomain | tunnel.subdomain | member-namespace child label |
| tunnel.allowIP | tunnel.allow_ip | additional visitor addresses or prefixes |
| tunnel.public | tunnel.public | allow every visitor address |
| tunnel.ephemeral | tunnel.ephemeral | remove the route when the tunnel stops |
| publish.target | publish.target | port or loopback HTTP origin |
| dev.command | dev.command | development process and arguments |
| dev.port | dev.port | optional fixed loopback port |
| dev.startupTimeout | dev.startup_timeout | maximum local startup wait |
host and subdomain are mutually exclusive. public and allowIP are mutually exclusive.
Targets must be a port or loopback HTTP origin. Ports range from 1 through 65535, and timeouts use
Go duration syntax such as 30s, 2m, or 1m30s.
When no host or subdomain is configured, tnl derives one from the Git worktree and service name. An explicit service hostname is appropriate for a long-lived shared callback; the derived default is safer for parallel feature worktrees.
discovery
From the current directory, tnl searches upward for the nearest tnl.yml, tnl.yaml, tnl.json,
or tnl.config.ts. Discovery stops at the Git worktree root. Outside Git, only the current
directory is searched.
Only one candidate can exist in a directory. Select a specific file when a directory intentionally contains more than one:
tnl --config config/tnl.preview.yml devTNL_CONFIG provides the same explicit selection. Use --no-config to disable discovery for one
command.
precedence
Configuration resolves one field at a time. Command options have highest precedence, followed by
supported TNL_* environment values, the named service override, root project defaults, and
built-in defaults. The worktree-derived hostname is the default only when no higher source supplies
a host or subdomain.
The team resolves separately: a named service team overrides the root project team; the project
value overrides the team saved by tnl team use; and a missing or stale saved selection falls back
to the personal team.
An explicit access token combined with a project-provided server is rejected unless the server is
also explicit on the command line or in TNL_SERVER. This prevents a credential from being sent to
an unexpected config-selected origin.
inspect the schema and resolved config
Editors can load the strict JSON Schema from https://tnl.dev/schema/v1.json. The hosted copy is
generated from the peer tnl repository's schema/v1.json; it is not maintained by hand. The schema
defines the versioned static YAML/JSON shape, including the required root envelope, but TypeScript
continues to use the package's camel-case types.
Show the selected file without evaluating it, then validate it:
tnl config path
tnl config checkUnknown fields, duplicate YAML keys, ambiguous config files, invalid service names, and mutually exclusive settings fail before authentication or route changes. Errors identify the selected file and invalid setting; target diagnostics link to contextual recovery help when startup reaches that boundary.
Continue with development workflow, Vite, or Next.js.