Drop a simple HTML app and serve it — internally via the home Caddy (html-bag) or publicly via Cloudflare Pages. One tool, two backends.
A bag is a directory of standalone HTML apps plus a way to serve it:
| bag | backend | served at | reach |
|---|---|---|---|
internal |
Caddy | tools.home.example.com |
LAN / Tailscale, instant |
public |
Pages | mini.example.com |
public internet, always-on |
An "app" is either a folder with index.html (plus assets) or a
standalone .html file. Anything whose name starts with tmp is treated
as scratch: shown under a separate Temp heading and gitignored.
webby is a Bun CLI (it uses Bun APIs, so it needs bun — not
node/npx). There's no binary to download; install straight from the repo:
bun install -g github:ankitson/webby # puts `webby` on your PATH
# one-off, no install (the bunx / npx equivalent):
bunx github:ankitson/webby whereConfigure it from the environment (see Configuration), then:
webby add <path> [--name N] [--tmp] [--bag N] # stage an app into a bag
webby pub <path> [--name N] [--tmp] # add to public bag + deploy
webby deploy --bag <name> # regenerate index + deploy (pages bags)
webby ls [--bag <name>] # list all bags, or one bag
webby rm <name> [--bag N] # remove an app
webby open <name> [--bag N] # print/open an app URL
webby domain <hostname> --bag <pages-bag> # attach a custom domain- Internal is a plain file copy into the
internal/dir — live immediately via Caddy's live mount, no deploy step. - Public copies into
public/, regenerates a static browseindex.html, and runswrangler pages deploy public/— deploy is just "push the directory". - The internal listing shows both bags: every public app is mirrored into
internal/as a relative symlink (../public/<app>), so the tools host lists internal + public apps in one flat page.public/stays the single source of truth; the symlinks are maintained automatically onpub/deploy.
webby add ./clock.html # → tools.home.example.com/clock.html (instant)
webby add ./dashboard --tmp # scratch folder app, internal
webby pub ./lissajous --name lissajous # publish a folder app to mini.example.com
webby deploy --bag public # re-push the whole public bagwebby reads everything from the environment — nothing is baked into the
code. Export the keys below, or point $WEBBY_ENV at a KEY=VALUE file (handy
with op inject/op run). When running from a clone, an in-repo .env.secret
(gitignored) is loaded automatically; see .env.secret.example for the keys.
CF_ACCOUNT_ID— Cloudflare account that owns the Pages projectCF_TOKEN_REF— 1Password reference for the API token (needs Pages: Edit); read viaop readat deploy time, never written to diskINTERNAL_URL,PUBLIC_URL— the domains each bag is served atINTERNAL_DIR,PUBLIC_DIR,PUBLIC_PROJECT— bag paths / Pages project name
export CF_ACCOUNT_ID=… CF_TOKEN_REF='op://…' INTERNAL_URL=… PUBLIC_URL=…
export INTERNAL_DIR=… PUBLIC_DIR=… PUBLIC_PROJECT=webby
webby where # prints the resolved bagsmini.example.com is a custom domain on a Cloudflare Pages project, so the
apps live on Cloudflare's edge — not on the home server (which has no public
ingress and suspends nightly). webby deploy pushes the public/ directory to
Pages; Cloudflare serves it globally with automatic TLS.
The webby token is Pages-scoped only. Attaching a custom domain also needs a
DNS record (CNAME <host> → <project>.pages.dev, proxied); creating that
requires a DNS-edit token on the zone, done once per domain.
- Built with Bun + TypeScript.
wrangleris invoked viabunx. - The old
html-bagrepo has been merged in: its apps now live ininternal/and the home Caddy mountsinternal/(+public/) directly./projects/html-bagis vestigial.