tnl.dev :: docs
config reference.
Look up project configuration fields, defaults, and precedence.
file discovery and formats
This is the project configuration contract for tnl. For tnld process settings, see Self-hosting.
Starting in the current directory, tnl searches upward to the Git worktree root (or stops in the current directory outside Git). At each directory it looks for tnl.yml, tnl.yaml, tnl.json, and tnl.config.ts. The nearest directory with a file wins; more than one of these files in that directory is an error. --config PATH selects a file explicitly, ahead of TNL_CONFIG; --no-config skips discovery and conflicts with --config. An explicitly selected TypeScript file must be named tnl.config.ts.
Static YAML and JSON files have version: 1 and put client settings under tnl. An optional root $schema may point at https://tnl.dev/schema/v1.json. TypeScript exports the client settings directly through defineConfig from @tnldotdev/tnl/config: no version or tnl wrapper. It may export a factory receiving cwd, env, and worktree; loading requires Node.js 22.18 or newer. Keep TypeScript configuration trusted: it is executed when loaded. tnl config path only selects; tnl config check loads and validates.
These three files have equivalent tnl settings. Pick one format, not all three:
import { defineConfig } from "@tnldotdev/tnl/config";
export default defineConfig({
server: "https://control.example.com",
team: "Studio",
tunnel: { allowIP: ["192.0.2.0/24"], requestLimit: 200 },
services: {
web: {
directory: "apps/web",
dev: { command: ["node", "server.js"], port: 3000, startupTimeout: "30s" },
publish: { target: 3000 },
},
},
});project defaults
All fields below can appear at the top of the TypeScript export or inside the static tnl object. TypeScript uses allowIP, allowAllIPs, requestLimit, and startupTimeout; static files use allow_ip, allow_all_ips, request_limit, and startup_timeout. Other field names match.
The static document has four top-level fields: version (required, exactly 1), $schema (optional editor schema URL), tnl (optional project client settings), and tnld (optional server process settings; see tnld reference). TypeScript accepts only the tnl fields below.
| Field | Meaning |
|---|---|
server | Control URL for the project. A service may override it. |
team | Team ID or unambiguous display name for the project. A service may override it. |
tunnel | Default public URL and tunnel settings, listed below. |
publish | Default target for tnl publish. |
dev | Default child command, port, and startup timeout for tnl dev. |
services | Map of up to 32 named project services. |
There is no project domain field. The selected team's ready default domain supplies the namespace; a full tunnel.host can select another ready domain available to that team. See Domains.
services
Service names are lowercase DNS labels beginning with a letter, up to 32 characters. Each services.NAME may set directory, server, team, tunnel, publish, and dev. directory is relative to the configuration file's directory, defaults to ., must exist, and cannot escape the project root through a path or symlink. tnl dev NAME runs its child command there. Root defaults merge with service settings field by field, including nested tunnel, publish, and dev fields. Arrays such as the IP list and child command replace the root value; they do not append.
With one named service, tnl dev and tnl publish select it automatically. With multiple services, name one. tnl publish 3000 or tnl publish http://127.0.0.1:3000 instead uses the supplied target.
dev and publish settings
| Field | Meaning |
|---|---|
dev.command | Nonempty array of executable and arguments used by tnl dev; a command after -- overrides it. |
dev.port | If set, requires the local service to listen on that port, 1–65535. tnl dev --port overrides it; framework integrations can report their target when no port is set. |
dev.startupTimeout / dev.startup_timeout | Time to wait for the service, greater than zero and at most 10 minutes; defaults to 2 minutes. Uses duration strings such as 30s or 1m30s. |
publish.target | Local HTTP URL or integer port, 1–65535, used if no target argument is supplied. |
public url settings
All tunnel fields can also be set under services.NAME.tunnel and apply to both dev and publish.
| TypeScript / YAML and JSON | Meaning |
|---|---|
host | Full public URL hostname. Must be in a ready domain available to the selected team; outside a member namespace it is a shared public URL requiring an admin or owner. |
subdomain | One lowercase DNS label beneath the current member namespace and default team domain. Mutually exclusive with host. |
allowIP / allow_ip | List of up to 63 distinct canonical IP addresses or prefixes allowed to visit; current client IP is added automatically. Use an empty list to allow just the current IP. |
allowAllIPs / allow_all_ips | true allows any visitor IP instead of an IP list. Cannot be true alongside allowIP. |
ephemeral | true removes the public URL when the tunnel stops; otherwise the public URL remains saved. |
requestLimit / request_limit | Maximum simultaneous requests forwarded by the publisher, including streams and upgrades; positive integer, default 500. |
Without a hostname setting, a non-ephemeral tunnel uses a service-and-worktree label beneath the member namespace. Two worktrees given the same fixed host or subdomain can contend for one public URL. An ephemeral tunnel without a hostname uses a generated label.
precedence and validation
tnl merges project defaults, then service overrides, then applies environment and command flags. For the server, an explicit --server or TNL_SERVER wins over the project setting; absent those, the selected server or https://control.tnl.dev applies. An explicit access token with a project-specified server requires an explicit --server or TNL_SERVER. For teams, --team / TNL_TEAM wins over the service or project team, then the saved selection or personal team applies. For tunnel fields, CLI values override matching environment and configured values; --host replaces a configured subdomain, --subdomain replaces a configured host, and explicit --allow-ip replaces a configured allowAllIPs. --allow-all-ips and --allow-ip cannot be combined.
Static files require version: 1, reject unknown fields, and validate values; TypeScript exports are validated against the same client contract. Run tnl config check after editing. See Configuration for a smaller starting point.