@abdullahharunozturk/localtld
v0.1.2
Published
Give your local projects real domains on dynamic ports — access panel.aaron.localtld instead of chasing ports.
Downloads
104
Maintainers
Readme
localtld
Give your local projects real domains on dynamic ports. Stop chasing ports — reach your app at panel.aaron.localtld.
// package.json
{ "localtld": "panel.aaron" }localtld run -- pnpm dev
# → http://panel.aaron.localtld (dynamic port — you never need to know it)Whatever port your dev server grabs (3000, 51234, doesn't matter), you always reach it at the same clean domain. Run hundreds of projects at once: no port collisions, nothing to track.
Zero-config, even if you've never heard of localtld
Wire it once in a project and it just works on any machine that has localtld set up — the developer doesn't need to know localtld exists:
// package.json
{
"localtld": "panel.aaron",
"scripts": {
"dev": "if command -v localtld >/dev/null 2>&1; then localtld run -- next dev; else next dev; fi"
},
"optionalDependencies": {
"@abdullahharunozturk/localtld": "^0.1.0"
}
}Now the normal workflow does everything:
pnpm install # on macOS the localtld binary lands in node_modules/.bin
pnpm devWhy optionalDependencies + the guard, not a plain devDependencies entry:
- localtld is macOS-only (
"os": ["darwin"]). As an optional dependency, Linux/Windows/CI skip it silently — a regulardependencies/devDependenciesentry would fail the whole install withEBADPLATFORM. - The
command -vguard lets thedevscript survive on any machine where the binary isn't present (non-macOS, or one that skipped it).
Result — zero action required from your teammates, and nobody needs to know localtld exists:
| Machine | pnpm install | pnpm dev |
|---------|----------------|------------|
| macOS, set up | installs localtld | panel.aaron.localtld |
| macOS, not set up | installs localtld | offers localtld setup, else localhost |
| Linux / Windows / CI | skips it (no error) | plain next dev on localhost |
Without adding a dependency
Don't want localtld in your devDependencies (or it isn't installed on every machine)? Guard the script so it degrades to a plain run when the binary is absent:
// package.json
{
"localtld": "panel.aaron",
"scripts": {
"dev": "if command -v localtld >/dev/null 2>&1; then localtld run -- next dev; else next dev; fi"
}
}- Machine has the
localtldbinary → pretty domain. - Machine doesn't → plain
next dev(localhost:PORT), no error, zero dependency.
command -v is POSIX sh (macOS/Linux). Use this when you want the repo to carry zero localtld dependency — no devDependencies entry, no assumption that any teammate has it installed. (Prefer the pinned devDependencies approach above if you'd rather everyone get the same domain automatically.)
How it works
localtld doesn't reinvent anything; it orchestrates two standard tools:
browser → panel.aaron.localtld
│ dnsmasq: *.localtld → 127.0.0.1 (+ macOS /etc/resolver)
▼
127.0.0.1:80 → Caddy (Host header → the right port)
▼
127.0.0.1:51234 ← your dev server (localtld assigned it via PORT env)- TLD is fixed (
.localtld) — identical on every machine, so shared configs, docs and bug reports always match. - Label is a project-level setting (
package.json) — one line, no TLD in it, so the repo stays portable. - Opt-in & graceful: without a system setup, projects just run on
localhost:PORT.
Environment variables (.env)
localtld changes the host your dev server is reachable at — it doesn't touch your app config. When services talk to each other by URL (a frontend calling an API, CORS origins, …), those URLs differ between the two worlds:
- localtld ON →
http://core.aaron.localtld(port 80, via Caddy) - localtld OFF →
http://localhost:3001
.env files are static and ${VAR} expansion isn't portable across tools, so the simplest pattern is to ship both and let each machine pick. Default to localhost in code so the project runs with zero config:
# .env.example
# localtld OFF (default):
CORE_API_URL=http://localhost:3001/api
# localtld ON:
# CORE_API_URL=http://core.aaron.localtld/api// default to localhost → works even without localtld
const base = process.env.CORE_API_URL ?? 'http://localhost:3001/api';A teammate copies .env.example → .env; if they use localtld, they swap the commented line. Keep the URL in .env (or the .env.example comment) rather than hardcoded in source — not every machine runs localtld, and the localhost fallback needs to keep working.
Install
# Homebrew (primary)
brew install abdullahharunozturk/localtld/localtld
# or npm (CLI is still `localtld`)
npm install -g @abdullahharunozturk/localtld
# or curl (coming soon — needs localtld.sh to be live)
# curl -fsSL https://localtld.sh | bash
localtld setup # configure dnsmasq + Caddy (asks for sudo)Requires macOS + Homebrew. Brew pulls in caddy, dnsmasq, and jq automatically; via npm/curl, setup installs them if missing.
Commands
| Command | What it does |
|---------|--------------|
| localtld setup | First-time setup: configure dnsmasq/Caddy |
| localtld run -- <cmd> | Run a project under its domain (falls back if not set up) |
| localtld list | Show active projects and their domains |
| localtld doctor | Check the health of your setup |
| localtld uninstall | Revert DNS/route changes |
Why .localtld is fixed
Every project gets <label>.localtld — the suffix is deliberately not configurable. One standard suffix means a shared package.json entry, a doc snippet or a bug report works identically on every machine, and .localtld is not a real TLD so it can never collide with a live website.
Want a different suffix? Fork and edit the single TLD= line at the top of bin/localtld. A built-in guard refuses forks that set a real TLD (.com, .dev, …), since every site under it would resolve to 127.0.0.1 on the user's machine.
Upgrading from an older version with a custom TLD? It is no longer read — the tool warns once and uses
.localtld; runlocaltld setupagain to re-wire DNS.
License
MIT © Abdullah Harun Öztürk
