[jdbb]
← blyg

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 dev is 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

  1. Install Node 22, then git clone https://github.com/blygger/blygger-studio and run npm ci.
  2. Create wrangler.local.jsonc (copy wrangler.jsonc, replace the placeholder database_id with a UUID) and add it to .git/info/exclude.
  3. Write .dev.vars with your secrets (chmod 600).
  4. npx wrangler d1 migrations apply DB --local --persist-to <data> -c wrangler.local.jsonc
  5. Install and enable the service and the poll timer. Make the VM's proxy public, log in to /studio, and set site_url.
  6. 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:

  1. Back up the data. Don't tar the live *.sqlite/-wal files, because a poll tick can write mid-read. Snapshot each database with sqlite3 <db> ".backup <copy>", run PRAGMA integrity_check on the copy, and tar the copies along with the R2 files.
  2. git merge --ff-only origin/main. Fast-forward only, so a dirty tree aborts the update.
  3. Regenerate wrangler.local.jsonc from the new wrangler.jsonc.
  4. npm ci, not npm install, which can rewrite the lockfile and dirty the tree.
  5. npx vitest run. If the tests fail, git reset --hard to the previous commit, run npm ci, and restart. The new code was already live through hot reload.
  6. 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 plain http://localhost curl won't send it from a cookie jar. Pass -H "Cookie: blyg_session=…" yourself.
  • /studio and /api are 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.