Zero-token, persistent Cloudflare tunnel CLI — the same public HTTPS URL for your localhost, every run.
TypeScript
0
16 commits
updated Jul 8, 2026
cfld = cloudflare local dev.
One command to give your localhost a permanent public HTTPS URL — no API tokens, no teardown.
cfld wraps cloudflared into a frictionless dev experience: the same custom URL every run (not a random trycloudflare.com one), authorized with a browser login (zero API tokens), reusing the same named tunnel and DNS record across restarts instead of churning them. It runs a live dashboard (URL, connection count, request/latency counters, tailing logs), auto-reconnects on edge blips, and guards against stealing a DNS record that belongs to another tunnel.
New here? Just run this — it walks you through everything, no account or config needed to start:
npx @cliftonc/cfld setup
setup is the front door. It checks cloudflared for you, then lets you pick:
trycloudflare.com URL). Great for a webhook, a demo, or just trying it.You don't even have to remember setup: the first time you run bare cfld in a fresh project, it drops you into this same guided flow so you're never stuck guessing.
Already know what you want? Skip straight to it:
# instant public URL in seconds — no account, no config
npx @cliftonc/cfld --quick 3000 # temporary trycloudflare.com URL
# the everyday command — reuse-or-create your persistent tunnel and go live
npx @cliftonc/cfld # detect your dev port, go live on the same URL
npx @cliftonc/cfld 3000 # explicit port
# or install once and use the short `cfld` command everywhere
npm i -g @cliftonc/cfld
cfld setup
cfld is built to get you going at zero → near-zero cost, and it's honest about the one thing it can't automate:
cfld --quick: free, no account, no domain. The URL is temporary and changes each run.cfld opens the browser to sign in (the same page has a Sign up link); there's no API token to create. Account signup itself can't be scripted — Cloudflare requires a browser, terms acceptance, and email verification.cloudflared?The persistent-tunnel flow normally means: create an API token in the dashboard, find your zone/account IDs, tunnel create, hand-write a config.yml, route dns, and remember not to delete anything. cfld collapses all of that into one idempotent command and adds the dev niceties (auto port detection, deterministic hostname, .env write-back, a live status view).
Compared to the alternatives:
cfld gives you a stable one.cfld uses browser login and preserves resources so the URL is truly persistent.cfld is npx-first and zero-token.cfld guides you, and cfld --quick works immediately with an ephemeral URL.cloudflared — used from your PATH if present, otherwise auto-downloaded via the optional cloudflared npm package.Every run reconciles four things, treating Cloudflare as the source of truth and local files as a cache:
~/.cfld/certs/<zone>.pem, obtained once via cloudflared tunnel login (browser, no token) and selected with --origincert so multiple domains coexist on one machine.cfld-<slug>) if it exists, created only if absent. Never deleted on exit.route dns --overwrite-dns points <slug>.<zone> at the tunnel (idempotent)../.env (PUBLIC_URL by default).A machine-wide registry at ~/.cfld/registry.json indexes every tunnel, so you can manage them from anywhere.
| Command | What it does |
|---|---|
cfld --quick [port] | Instant trycloudflare.com URL — no account, no cert/domain (temporary). |
cfld setup | Guided first-run — pick the instant free URL or set up a persistent one. |
cfld [port] | Ensure + run a persistent tunnel (reuses the same URL). |
cfld login [--reauth] | Authorize with Cloudflare in your browser. --reauth adds another domain. |
cfld init | Configure a persistent tunnel without running it. |
cfld list | List all tunnels across projects/domains. |
cfld status [name] | Show details for a tunnel. |
cfld up <name> | Run a registered tunnel by name from anywhere. |
cfld destroy [name] | Delete a tunnel (explicit, opt-in). |
cfld service <action> [name] | Always-on background service: install/uninstall/start/stop/status. |
cfld doctor | Diagnose your setup. |
--name <slug> Override the project name (default: package.json name / dir)
--host <fqdn> Explicit public hostname (e.g. api.example.com)
--zone <domain> Cloudflare domain to use (e.g. example.com)
--env <KEY> .env key to write the URL to (default: PUBLIC_URL)
--no-env Don't write the URL to .env
--force Skip confirmation (destroy)
Everything is inferred by default, but you can commit intent to package.json under a "cfld" key. Authored values win over the machine-local .cfld.json cache (so edits take effect immediately); only the resolved tunnel UUID comes from the cache.
{
"cfld": {
"zone": "example.com",
"name": "myapp", // → myapp.example.com
"envKey": "PUBLIC_URL"
}
}
Expose an API and a web app at once with a routes array:
{
"cfld": {
"zone": "example.com",
"routes": [
{ "name": "app", "port": 3000 }, // app.example.com → :3000
{ "name": "api", "port": 8080, "path": "/v1" }, // api.example.com/v1 → :8080
{ "host": "hooks.example.com", "port": 4000 } // explicit hostname
]
}
}
The first route is the primary (used for the dashboard and .env). Every hostname is routed and DNS-guarded.
The foreground cfld is the dev default. To keep a tunnel up across logout/reboot, install it as a background service (macOS LaunchAgent / Linux systemd user unit):
cfld service install # register + start
cfld service status
cfld service uninstall # stop + remove (tunnel + DNS preserved)
Windows service management is not supported — run cfld in the foreground there.
Give cfld your dev command after a -- and it runs both as one managed unit:
cfld 3000 -- next dev
cfld will:
PUBLIC_URL already in its environment (so it can read its own public URL for webhooks/OAuth callbacks — no .env round-trip needed);:3000 to actually listen before flipping to ● LIVE, so the URL never serves a 502 the moment you reveal it;The pass-through form (-- <cmd>) runs your command directly. If you need shell features (pipes, &&, globs), use the string form instead:
cfld 5173 --exec "npm run dev"
This works with --quick too, when you don't have a domain set up yet:
cfld --quick 3000 -- next dev
Wire it into package.json so the whole team gets the same public URL:
{
"scripts": {
"dev": "next dev",
"dev:tunnel": "cfld 3000 -- next dev"
}
}
Tip: commit a
"cfld": { "zone": "example.com", "name": "myapp" }block topackage.jsonso the URL is deterministic for the whole team — everyone'sdev:tunnelmaps tomyapp.example.com.
cfld also works fine standalone — it only needs the port, and tolerates the app not being up yet (502 until it starts, then recovers). Run them with concurrently:
"dev:tunnel": "concurrently -k -n app,cfld -c blue,magenta \"npm:dev\" \"cfld 3000\""
or plain shell: cfld 3000 & next dev.
npm install
npm run build # tsup → dist/
npm test # vitest unit + integration tests
npm run typecheck # tsc --noEmit
The primary end-to-end check needs no domain or paid plan: cfld --quick against a local server, then fetch the resulting URL and assert the response round-trips through Cloudflare's edge.
MIT
16 commits
TypeScript
100.0%
Zero-token, persistent Cloudflare tunnel CLI — the same public HTTPS URL for your localhost, every run.
TypeScript
0
16 commits
updated Jul 8, 2026
cfld = cloudflare local dev.
One command to give your localhost a permanent public HTTPS URL — no API tokens, no teardown.
cfld wraps cloudflared into a frictionless dev experience: the same custom URL every run (not a random trycloudflare.com one), authorized with a browser login (zero API tokens), reusing the same named tunnel and DNS record across restarts instead of churning them. It runs a live dashboard (URL, connection count, request/latency counters, tailing logs), auto-reconnects on edge blips, and guards against stealing a DNS record that belongs to another tunnel.
New here? Just run this — it walks you through everything, no account or config needed to start:
npx @cliftonc/cfld setup
setup is the front door. It checks cloudflared for you, then lets you pick:
trycloudflare.com URL). Great for a webhook, a demo, or just trying it.You don't even have to remember setup: the first time you run bare cfld in a fresh project, it drops you into this same guided flow so you're never stuck guessing.
Already know what you want? Skip straight to it:
# instant public URL in seconds — no account, no config
npx @cliftonc/cfld --quick 3000 # temporary trycloudflare.com URL
# the everyday command — reuse-or-create your persistent tunnel and go live
npx @cliftonc/cfld # detect your dev port, go live on the same URL
npx @cliftonc/cfld 3000 # explicit port
# or install once and use the short `cfld` command everywhere
npm i -g @cliftonc/cfld
cfld setup
cfld is built to get you going at zero → near-zero cost, and it's honest about the one thing it can't automate:
cfld --quick: free, no account, no domain. The URL is temporary and changes each run.cfld opens the browser to sign in (the same page has a Sign up link); there's no API token to create. Account signup itself can't be scripted — Cloudflare requires a browser, terms acceptance, and email verification.cloudflared?The persistent-tunnel flow normally means: create an API token in the dashboard, find your zone/account IDs, tunnel create, hand-write a config.yml, route dns, and remember not to delete anything. cfld collapses all of that into one idempotent command and adds the dev niceties (auto port detection, deterministic hostname, .env write-back, a live status view).
Compared to the alternatives:
cfld gives you a stable one.cfld uses browser login and preserves resources so the URL is truly persistent.cfld is npx-first and zero-token.cfld guides you, and cfld --quick works immediately with an ephemeral URL.cloudflared — used from your PATH if present, otherwise auto-downloaded via the optional cloudflared npm package.Every run reconciles four things, treating Cloudflare as the source of truth and local files as a cache:
~/.cfld/certs/<zone>.pem, obtained once via cloudflared tunnel login (browser, no token) and selected with --origincert so multiple domains coexist on one machine.cfld-<slug>) if it exists, created only if absent. Never deleted on exit.route dns --overwrite-dns points <slug>.<zone> at the tunnel (idempotent)../.env (PUBLIC_URL by default).A machine-wide registry at ~/.cfld/registry.json indexes every tunnel, so you can manage them from anywhere.
| Command | What it does |
|---|---|
cfld --quick [port] | Instant trycloudflare.com URL — no account, no cert/domain (temporary). |
cfld setup | Guided first-run — pick the instant free URL or set up a persistent one. |
cfld [port] | Ensure + run a persistent tunnel (reuses the same URL). |
cfld login [--reauth] | Authorize with Cloudflare in your browser. --reauth adds another domain. |
cfld init | Configure a persistent tunnel without running it. |
cfld list | List all tunnels across projects/domains. |
cfld status [name] | Show details for a tunnel. |
cfld up <name> | Run a registered tunnel by name from anywhere. |
cfld destroy [name] | Delete a tunnel (explicit, opt-in). |
cfld service <action> [name] | Always-on background service: install/uninstall/start/stop/status. |
cfld doctor | Diagnose your setup. |
--name <slug> Override the project name (default: package.json name / dir)
--host <fqdn> Explicit public hostname (e.g. api.example.com)
--zone <domain> Cloudflare domain to use (e.g. example.com)
--env <KEY> .env key to write the URL to (default: PUBLIC_URL)
--no-env Don't write the URL to .env
--force Skip confirmation (destroy)
Everything is inferred by default, but you can commit intent to package.json under a "cfld" key. Authored values win over the machine-local .cfld.json cache (so edits take effect immediately); only the resolved tunnel UUID comes from the cache.
{
"cfld": {
"zone": "example.com",
"name": "myapp", // → myapp.example.com
"envKey": "PUBLIC_URL"
}
}
Expose an API and a web app at once with a routes array:
{
"cfld": {
"zone": "example.com",
"routes": [
{ "name": "app", "port": 3000 }, // app.example.com → :3000
{ "name": "api", "port": 8080, "path": "/v1" }, // api.example.com/v1 → :8080
{ "host": "hooks.example.com", "port": 4000 } // explicit hostname
]
}
}
The first route is the primary (used for the dashboard and .env). Every hostname is routed and DNS-guarded.
The foreground cfld is the dev default. To keep a tunnel up across logout/reboot, install it as a background service (macOS LaunchAgent / Linux systemd user unit):
cfld service install # register + start
cfld service status
cfld service uninstall # stop + remove (tunnel + DNS preserved)
Windows service management is not supported — run cfld in the foreground there.
Give cfld your dev command after a -- and it runs both as one managed unit:
cfld 3000 -- next dev
cfld will:
PUBLIC_URL already in its environment (so it can read its own public URL for webhooks/OAuth callbacks — no .env round-trip needed);:3000 to actually listen before flipping to ● LIVE, so the URL never serves a 502 the moment you reveal it;The pass-through form (-- <cmd>) runs your command directly. If you need shell features (pipes, &&, globs), use the string form instead:
cfld 5173 --exec "npm run dev"
This works with --quick too, when you don't have a domain set up yet:
cfld --quick 3000 -- next dev
Wire it into package.json so the whole team gets the same public URL:
{
"scripts": {
"dev": "next dev",
"dev:tunnel": "cfld 3000 -- next dev"
}
}
Tip: commit a
"cfld": { "zone": "example.com", "name": "myapp" }block topackage.jsonso the URL is deterministic for the whole team — everyone'sdev:tunnelmaps tomyapp.example.com.
cfld also works fine standalone — it only needs the port, and tolerates the app not being up yet (502 until it starts, then recovers). Run them with concurrently:
"dev:tunnel": "concurrently -k -n app,cfld -c blue,magenta \"npm:dev\" \"cfld 3000\""
or plain shell: cfld 3000 & next dev.
npm install
npm run build # tsup → dist/
npm test # vitest unit + integration tests
npm run typecheck # tsc --noEmit
The primary end-to-end check needs no domain or paid plan: cfld --quick against a local server, then fetch the resulting URL and assert the response round-trips through Cloudflare's edge.
MIT
16 commits
TypeScript
100.0%