# How this blyg runs (without Cloudflare)

The Blygger reference client ([blygger-studio](https://github.com/blygger/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](https://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](/_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

```ini
[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):

```ini
# 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](https://www.jdbb.net/) and serves this page, without patching the Worker. Studio, the API, feeds and media pass through untouched.
