How this blyg runs (without Cloudflare)
The Blygger reference client (blygger-studio) is written as a Cloudflare Worker: D1 database, R2 media bucket, cron triggers. Its docs assume you wrangler deploy it to a Cloudflare account.
This blyg runs the same, unmodified Worker code (plain upstream main, no local patches) on a single Linux VM (exe.dev) instead. It uses wrangler dev, which runs the Worker in workerd (Cloudflare's open-source runtime) with local, file-backed D1 and R2. No Cloudflare account is involved. The wire output (feed.xml, blyg.json, items/…) is what the reference client produces, so to the rest of the network it is just another conformant node.
The raw Markdown of this page is at /_jdbb/how-this-blyg-runs.md. Paste it to another machine or agent as a starting point.
Why
- No divergence from the reference code. Upstream fixes arrive as a fast-forward. Any bug you hit is a real reference-client bug that you can report upstream.
- Everything on one box. The data is plain SQLite files plus media files on disk, so it's easy to back up, inspect, and move.
- No Cloudflare account, keys, or routes. The VM host's HTTPS proxy provides TLS and a hostname.
- Trade-off:
wrangler devis a dev server, not Cloudflare's edge. You get one process on one VM and no CDN. That's fine for a single-author blyg.
Translating the upstream docs
| Cloudflare deployment (upstream docs) | Self-hosted on a VM |
|---|---|
wrangler deploy |
a systemd service running wrangler dev permanently |
| D1 database (remote) | SQLite under <data>/v3/d1/ (--persist-to <data>) |
| R2 media bucket | files under <data>/v3/r2/ |
wrangler secret put … |
.dev.vars in the checkout (git-ignored, chmod 600): OWNER_PASSWORD, COOKIE_SECRET, AI_PROVIDER_KEY |
database_id from npm run init |
a git-excluded wrangler.local.jsonc: a copy of upstream wrangler.jsonc with the placeholder id replaced by any fixed UUID, regenerated on every update |
vars.MOUNT |
--var MOUNT: on the command line (empty = domain root) |
| Cron triggers | a systemd timer that curls http://localhost:<port>/__scheduled?cron=… (needs --test-scheduled) |
| Routes / custom domain | the VM's HTTPS proxy → VM port; a custom domain is a DNS CNAME to the VM hostname |
d1 migrations apply --remote |
d1 migrations apply DB --local --persist-to <data> -c wrangler.local.jsonc |
npm run init / upgrade / deploy |
not used; a small update script (below) |
Rule of thumb: anything in the upstream docs that mentions --remote, wrangler deploy, wrangler secret put, account_id, deploy-targets.json or npm run init doesn't apply. Everything about the wire format, Studio, and src/ applies unchanged.
The service
[Unit]
Description=Blygger Studio via wrangler dev / workerd
After=network-online.target
[Service]
User=<you>
WorkingDirectory=<checkout>
Environment=WRANGLER_SEND_METRICS=false
ExecStart=/usr/bin/npx wrangler dev -c wrangler.local.jsonc --ip 0.0.0.0 --port 8000 \
--persist-to <data> --var MOUNT: \
--local-upstream <your.domain> --upstream-protocol https \
--test-scheduled --show-interactive-dev-session=false
Restart=always
[Install]
WantedBy=multi-user.target
--local-upstream <your.domain> --upstream-protocol https is required. Studio and /api reject any request whose URL isn't https:. Behind a TLS-terminating proxy, wrangler dev would otherwise see http://…. With these flags the Worker sees https://<your.domain>/… for every request. Keep this domain in sync with the site_url setting in Studio.
The poll timer (every 15 minutes):
# blyg-poll.service
[Service]
Type=oneshot
ExecStart=/usr/bin/curl -fsS "http://localhost:8000/__scheduled?cron=*/15+*+*+*+*"
# blyg-poll.timer
[Timer]
OnCalendar=*:0/15
Upstream declares three crons. Firing only the 15-minute one is enough: it polls subscriptions, and its 00:00 UTC tick also runs the daily housekeeping. The every-minute tick only pre-warms a stale-while-revalidate feed cache.
First install
- Install Node 22, then
git clone https://github.com/blygger/blygger-studioand runnpm ci. - Create
wrangler.local.jsonc(copywrangler.jsonc, replace the placeholderdatabase_idwith a UUID) and add it to.git/info/exclude. - Write
.dev.varswith your secrets (chmod 600). npx wrangler d1 migrations apply DB --local --persist-to <data> -c wrangler.local.jsonc- Install and enable the service and the poll timer. Make the VM's proxy public, log in to
/studio, and setsite_url. - In Studio → Settings, choose an AI model. There is no default, so TK generation fails until you set one.
Updating safely
wrangler dev hot-reloads: any change to the checkout goes live immediately. So never edit the checkout in place (use a git worktree elsewhere), and run updates as a script that can roll back:
- Back up the data. Don't
tarthe live*.sqlite/-walfiles, because a poll tick can write mid-read. Snapshot each database withsqlite3 <db> ".backup <copy>", runPRAGMA integrity_checkon the copy, and tar the copies along with the R2 files. git merge --ff-only origin/main. Fast-forward only, so a dirty tree aborts the update.- Regenerate
wrangler.local.jsoncfrom the newwrangler.jsonc. npm ci, notnpm install, which can rewrite the lockfile and dirty the tree.npx vitest run. If the tests fail,git reset --hardto the previous commit, runnpm ci, and restart. The new code was already live through hot reload.- Apply the local D1 migrations, then restart the service.
We run this nightly from a timer that emails the outcome. Inside scripts with set -o pipefail, avoid cmd | head: SIGPIPE (exit 141) can silently kill them.
Gotchas
- The session cookie is
Secure, so over plainhttp://localhostcurl won't send it from a cookie jar. Pass-H "Cookie: blyg_session=…"yourself. /studioand/apiare host-rooted, so mount at the domain root on a dedicated hostname.- Only per-request lines reach the journal. Error bodies aren't logged.
- Webmention verification is origin-exact. If you change domains, mentions that target the old URLs won't verify. RSS guids don't contain the domain, so readers don't see duplicates.
- The protocol is unstable before 1.0, including the wire format. Read the commit list before updating.
This blyg's extras (optional)
A small Node proxy sits in front of wrangler (:8000 → :8001). It reskins the public HTML pages to match www.jdbb.net and serves this page, without patching the Worker. Studio, the API, feeds and media pass through untouched.