nz.
Push a directory of HTML, get a url back. A small CLI for macOS, Linux and Windows, one Cloudflare Worker, and one KV entry per site.
$ nz push ./report --ttl 7d --password meow
expires 2026-11-10T19:40:02.000Z, password protected
https://k3x9q.example.com/
$ nz push ./report # same path, same url, new content
$ nz update k3x9q --no-password # policy only, content kept
The files
| file | what | lines |
worker.js | Cloudflare Worker: plain JS, no dependencies, no build | 170 |
nz | the CLI: POSIX sh, using only tar and curl | 95 |
nz.ps1 | the same CLI for PowerShell, using the tar.exe and curl.exe built into Windows | 146 |
test.sh | end-to-end checks against wrangler dev | 61 |
Architecture
A site is a single KV entry. Pushing writes it once, and serving a page reads it once. KV's built-in expiration deletes it when the TTL runs out.
One entry holds everything about a site:
key k3x9q 5 chars, a-z0-9
value <ustar bytes> the whole site, 25 MiB max
metadata { exp: 1795895602, unix seconds, or null for never
pw: "<salt>.<pbkdf2>" } absent when public
expiration exp KV deletes the entry for us
Push
nz finds files (skipping dotfiles and node_modules), tars them as ustar, and writes headers to a temporary file so the token and password stay out of ps.
- If
~/.config/nz/sites has an id for this path, it PUTs to that id. If that site has expired (404), or there is no id yet, it POSTs a new one and records id<TAB>path.
- The Worker checks that the tar parses, works out the expiry and password, and writes the entry with one
put. Headers you leave out on update keep their current values, and an empty body keeps the current content.
Serve
- Every site is
https://<id>.domain/, so each one is its own browser origin, with its own cookies and storage.
- The Worker reads the entry, walks the ustar headers (512-byte blocks, octal sizes), and returns the matching file.
/ maps to index.html, and a directory without a trailing slash gets a 308 redirect.
Password
- Only a PBKDF2-SHA256 verifier is stored, never the password. A locked site shows a small form, and unlock attempts are limited to 5 per minute per IP and site.
- Unlocking sets a host-only
__Host-nz cookie, valid for 7 days, whose HMAC covers the id, the expiry and the verifier. Changing the password changes the verifier, which invalidates every outstanding cookie.
API
The CLI is a thin wrapper around this, so curl works just as well:
tar --format=ustar -cf - -C site . |
curl -H "authorization: Bearer $TOKEN" \
-H 'nz-ttl: 7d' -H 'nz-password: meow' \
--data-binary @- https://example.com/ # 201, new site
curl -X PUT ... https://example.com/k3x9q # 200, update
curl -X PUT -H 'nz-password;' ... # empty: no password
The response is the url on line one and a details line on line two. TTL takes 30m, 12h, 14d, 2w or never, and the default is DEFAULT_TTL (14d).
Install
wrangler kv namespace create SITES # id into wrangler.jsonc
# set DOMAIN and routes; add a proxied DNS record: * AAAA 100::
wrangler secret put TOKEN
wrangler deploy
ln -s "$PWD/nz" ~/bin/nz
printf 'NZ_SERVER=https://example.com\nNZ_TOKEN=...\n' > ~/.config/nz/config
On Windows 10 or later, put nz.ps1 on your PATH and write the same config:
New-Item -ItemType Directory -Force ~\.config\nz | Out-Null
"NZ_SERVER=https://example.com`nNZ_TOKEN=..." | Set-Content -Encoding ascii ~\.config\nz\config
nz.ps1 push .\site --ttl 7d --password meow