1# Chapter 2: the Worker, where everything actually happens 2 3Everything you see at <https://lmjtfy.fun> comes out of this 4folder. It is one Cloudflare Worker: a small program Cloudflare runs at its 5edge, started fresh for each request in milliseconds, written here in Rust 6and compiled to WebAssembly. There is no server to keep running. A request 7arrives, the Worker answers it, and that is that. 8 9Two things have to outlive a single request: what was already asked (so it 10is never asked again) and how much of today's budget is left. For those the 11Worker has two Durable Objects, which are the closest Cloudflare comes to a 12tiny server with a memory: one object per name, anywhere in the world, with 13its own SQLite. The Worker talks to them; they remember. 14 15```mermaid 16flowchart LR 17 V["Visitor's browser"] -->|"/, /ask, /gate"| W["Worker (src/lib.rs)"] 18 V -->|"/live (socket)"| A 19 G["git clone"] -->|"/lmjtfy.git"| W 20 W --> A["Archive object: every call, the feed, the sockets"] 21 W --> B["Budget object: today's spend, per-visitor limits"] 22 A --> J["Jev (TypeSafe)"] 23 A --> L["LLM (Workers AI)"] 24 W --> GH["GitHub (read-only token)"] 25``` 26 27## What it serves 28 29| Address | What happens | 30| --- | --- | 31| `/` and `/?q=...` | The page. With `?q=`, it types the question in for you and asks. | 32| `POST /ask` | Answers a question as a stream: the whole transcript, re-sent as each call finishes. | 33| `POST /rate` | A browser's 👍 or 👎 on Jev's answer, and the votes after it. | 34| `POST /seen` | A page's report of itself as it is left: how long it was in view, how far down, the screen; or a link followed off the site. Kept, and answered with nothing. | 35| `POST /gate` | The facts request, run as you type, 300 ms after each pause, so the answer starts from what Jev already said. | 36| `/feed` | The next page of "asked lately", which the feed asks for when it is scrolled to its end. It reads the archive and asks nobody anything. | 37| `/live` | A WebSocket each open page holds: how many are online, toasts, live feeds, and "a new version is live". | 38| `/rules` | The rules engine with every fact clickable, asking nobody anything. | 39| `/rules.svg` | The rules drawn as a standalone picture, for the docs (chapter 6). | 40| `/card.png` | The picture a link unfurls with (chapter 11). | 41| `/icons/<name>.png` | The explorer's file and folder icons (chapter 15⅝). | 42| `/lmjtfy.git` | `git clone` it, or open it in a browser and read the code (you are probably here). | 43| `/jevcrates.git`, `/postjevsql.git`, `/jevsnes.git`, `/jevhooks.git` | The client lmjtfy shares with the owner's three other Jev projects, and those projects, served the same way. | 44 45> **Try it.** Open <https://lmjtfy.fun/rules> and click 46> `answerable` until it says no. Watch every rule but one go grey. That is 47> the engine from chapter 6 running in your browser's address bar. 48 49## Live: who is here, and what just happened 50 51Every browser with the site open keeps one socket to `/live`, held by the 52archive object. Its sockets are "hibernatable": while nothing happens, the 53object can sleep and nothing is billed, and the sockets stay open. 54 55One socket per browser, not per tab: the socket belongs to a SharedWorker 56(`src/live.js`), a script the browser runs once for all of a site's tabs 57and keeps while any of them is open. Each tab talks to it, and it passes on 58everything the archive sends; a tab that opens later is handed the current 59count and places at once. A tab the browser freezes in the background 60cannot take the socket down with it, which is what goes wrong when one tab 61holds the socket for the rest. A browser without SharedWorker opens a socket 62per tab, as every page used to. 63 64> **Aside.** Pages of different builds use different workers (the build is 65> in the worker's address), so after a deploy a reloaded tab gets a fresh 66> one while a tab from before keeps its own until it reloads too. 67 68It pushes four things: 69 70- **How many browsers are open, and where**, shown as "N online" in the top 71 bar; hover it for a flag and a place for each, most first. The place is 72 Cloudflare's: every request arrives with a rough country and city, and the 73 Worker passes them to the archive when a page connects (in headers it sets 74 over any the page sent, so a page cannot name its own). The archive keeps 75 them on that page's socket while it is open, for the list. (What is kept 76 for good is below, in [What is kept about visitors](#what-is-kept-about-visitors).) Pages 77 are told at most once a second, so a deploy, which reconnects every page 78 at once, is one update rather than one per page. A page that reconnects 79 while the deploy is still reaching Cloudflare's edge can come through the 80 Worker from before, which passes no place; ten seconds on, when the deploy 81 has settled, the archive asks it to reconnect (close code 1012), and it 82 comes back placed. 83- **A toast when someone asks a question or clones the code.** A question 84 the feed may show is shown with Jev's answer; any other is "someone asked 85 Jev something". The page that asked does not get its own toast. The site 86 sends pages nothing else about who is connected: no address, no browser, 87 no history. 88- **The feeds themselves.** After each toast the archive sends the home 89 page's feeds as HTML elements with ids, and a page replaces the ones it 90 has: most asked and so far whole. A question just asked goes to the top 91 of asked lately, taking its line from wherever it was, so the older lines 92 a page has scrolled in stay put. No refresh. 93- **The build.** Every page carries the commit its Worker was built from 94 (`build.rs`). A deploy restarts the archive object, every socket closes, 95 every page reconnects, and the archive tells each one the build that is 96 live now. A page from an older build shows a toast that stays, with a 97 Reload button. If the deployed commit has a `Release-Note:` trailer, an 98 older page also gets that line as a toast, so the people on the site hear 99 what changed. 100 101## The code pages 102 103 git clone --recurse-submodules https://lmjtfy.fun/lmjtfy.git 104 105The repository is private on GitHub, and is shared from here instead, so 106nobody needs a GitHub account and nothing is announced. The Worker speaks 107git's smart HTTP (`src/clone.rs`): the two requests a clone or fetch makes, 108`info/refs?service=git-upload-pack` and `git-upload-pack`, are forwarded to 109GitHub with a fine-grained token that can read `deizel/lmjtfy` and 110`deizel/jevcrates` and nothing else. A push is refused here, and GitHub 111would refuse it anyway. jevcrates is served beside it at `/jevcrates.git`, 112because `.gitmodules` names it by the relative URL `../jevcrates.git`, which 113git resolves against wherever lmjtfy was cloned from. The owner's three other 114Jev projects (postjevsql, jevsnes, jevhooks) are served too, and name 115jevcrates the same way, so each clones with its submodule from here. 116 117Clones and pulls are counted, anonymously, per repository; "So far" shows 118lmjtfy's, and a clone of any project but jevcrates is toasted. jevcrates is 119fetched along with every project cloned with its submodules, so its toasts 120would double up. A pull names the commits it 121already has (`have` lines) and a clone does not; a fetch counts on the round 122GitHub answers with the pack, so a long negotiation counts once and a pull 123with nothing new is not counted. 124 125Git only ever asks for `lmjtfy.git/info/refs` and `lmjtfy.git/git-upload-pack`, 126so every other path under `/lmjtfy.git` is free for people. The code pages 127(`src/browse.rs`) are laid out like an editor, full width. On the left, a 128sidebar of panels: the explorer (the whole tree, jevcrates included, 129opened on the way to the page you are on), the outline of the page's 130headings, the clone command, the latest commit, and every repository served 131here; jevcrates' pages add the `Cargo.toml` lines for depending on it, pinned 132to its latest commit. On the right, the page: 133a folder's README and CLAUDE.md as two tabs, "for people" and "for agents", 134between a ◀ tab for the chapter before and a ▶ tab for the chapter after 135(from the chapter's own last line, or, in a repository with no guide, the 136next folder with a README in a depth-first walk), 137with their diagrams drawn (click one to enlarge it); a source file 138with its comments rendered on the left and the code they are about on the 139right, coloured (chapter 12½), or rendered if it is markdown, each with a tab 140for its source top to bottom; `?raw` 141gives a file as it is. `/lmjtfy.git` itself is the root folder, whose 142README is the prologue. On a phone the sidebar moves below the page. 143 144Everything is read from GitHub's API with the same token, kept a minute per 145isolate: folders and files from the contents API, the explorer's tree in one 146request from the git trees API. jevcrates is browsed under 147`third-party/jevcrates/` at the commit lmjtfy pins. 148 149## Storage 150 151Two Durable Objects keep everything that outlasts a request. Each is one 152object for the whole site (`id_from_name`), so every visitor reads the same 153rows. 154 155### The archive (`src/archive.rs`) 156 157A SQLite database, changed only by adding a step to `MIGRATIONS`. The tables 158share no keys, `events.browser` aside: an `asked` row's answers are what the `versions` behind it said, 159copied, and `counts` is a tally. 160 161`askers` is how a question counts each browser once. A browser's first page 162gives it a random id in a cookie (`lmjtfy_browser`, a year); when it asks, the 163Worker hashes the id with the question and the archive keeps only that. A 164browser asking again, or opening its own question from the feed, finds its 165row and is not counted, toasted or moved up the feed again. The rows of two 166questions from one browser have nothing in common, and the id cannot be had 167back from one. So the key says nothing about who asked; the `browser` column 168beside it, which the owner's backend reads, is the id itself. An ask with no cookie (a 169script) counts every time, as before. Counts from before 2026-10-02 stay as 170they were. 171 172`ratings` holds the 👍 and 👎 under each answer ("Was Jev right?"): one vote 173per browser, keyed the same way, and pressed again to take it back. A vote is 174on the answer as it was kept when it was cast, by the hash of its answers, so 175if the question is answered differently later, that answer starts with no 176votes and the old one keeps its own. 177 178```mermaid 179erDiagram 180 versions { 181 text sent_to PK "Jev's endpoint, or a Workers AI model id" 182 text request PK "the exact body sent" 183 integer version PK "1, 2, ...: each time it was sent, newest last" 184 text response "the exact body that came back" 185 text request_id "Jev's id for the call, if it gave one" 186 integer attempts "tries the client made" 187 real took_ms 188 real answered_ms "when it was answered" 189 } 190 asked { 191 text input PK "the question, cleaned" 192 text answers "JSON: one Answer per question Jev answered" 193 real asked_ms "last asked" 194 integer times "how often it was asked" 195 integer listed "1 if the feed may show it (Jev's fit fact)" 196 integer llm "1 if an LLM had to be asked" 197 integer moderated "the owner's say: 1 show, 0 hide, NULL leave it to listed" 198 } 199 events { 200 integer id PK 201 real at_ms "when" 202 text what "view, answer, gate, vote, more, card, fetch, moved, live, left, read, out" 203 text method 204 text host 205 text path 206 text query "as it came" 207 text input "the question typed, asked or voted on" 208 text detail "how an ask ended, which way a vote went, clone or pull" 209 real status 210 real sent "calls sent for it" 211 real kept "calls answered from versions" 212 real llm 213 real took_ms "an answer's time, or how long a page stayed" 214 real first "1 on a browser's first page ever" 215 real daily "1 on its first page of the UTC day" 216 real session "1 on its first page in half an hour" 217 text referrer "the page that linked here, whole" 218 text source "utm_source or ref" 219 text client "browser, git, bot, other" 220 text family "Chrome, Firefox, ..." 221 text os 222 text device "mobile or desktop" 223 text language "Accept-Language" 224 text agent "User-Agent" 225 text browser "the lmjtfy_browser cookie" 226 text ip 227 text country 228 text region 229 text city 230 text postcode 231 text timezone 232 real latitude 233 real longitude 234 real asn "the network's number" 235 text network "whose network: the ISP" 236 text colo "Cloudflare's data centre" 237 text protocol "HTTP and TLS versions" 238 text medium "utm_medium" 239 text campaign "utm_campaign" 240 text screen "1920x1080, as the page says" 241 text viewport "the window" 242 real scroll "how far down the page was seen, 0 to 1" 243 } 244 counts { 245 text name PK "sent, kept, clone:lmjtfy.git, pull:lmjtfy.git, ..." 246 integer n 247 } 248 askers { 249 text input PK "the question" 250 text who PK "SHA-256 of a browser's id and the question" 251 text browser "the browser's id itself, for the owner's reading" 252 } 253 ratings { 254 text input PK "the question" 255 text answer PK "SHA-256 of the answers as kept" 256 text who PK "as in askers" 257 integer vote "1 right, -1 wrong" 258 text browser "as in askers" 259 } 260 migrated { 261 integer version PK "MIGRATIONS steps run" 262 } 263``` 264 265It also holds, in memory only, the calls on the wire (so identical asks wait 266on one) and the `/live` sockets of every open page. 267 268### The budgets (`src/meter.rs`) 269 270Key-value storage, a JSON value per key. 271 272```mermaid 273erDiagram 274 neurons { 275 integer day "days since 1970, UTC" 276 real used "Workers AI neurons spent that day" 277 } 278 jev_dollars { 279 integer day 280 real used "dollars of Jev spent that day" 281 } 282 account { 283 integer day 284 real at_ms "when Cloudflare's figure was read" 285 real neurons "the whole account's neurons that day" 286 } 287``` 288 289Each visitor's count for the minute (`budget::Visits`) is in memory and 290written nowhere by the site. 291 292## What is kept about visitors 293 294Everything a request says, for the owner alone. Until 2026-10-03 the site 295kept nothing about who asked; the owner then ruled the other way ("anywhere 296in the app where we are dropping data we should plug"), so this is the true 297account now. 298 299Every request worth keeping is a row of `events` in the archive 300(`packages/archive/src/event.rs`): a page viewed, an ask and how it ended, 301each as-you-type request, a vote, a clone, a redirect from an old address, a 302page connecting and leaving, and what each page reports of itself as it is 303left (how long it was in view, how far down it was read, the screen) or when 304a link off the site is followed. A row has the question, the visitor's address, 305their browser's id (the `lmjtfy_browser` cookie, which ties one browser's 306rows together), the user agent, the referring page, and what Cloudflare says 307of where the request came from: the network (the ISP), country, region, 308city, postcode, timezone and coordinates. The Worker writes the row after 309the response has gone (`wait_until`), so keeping it delays nobody. 310 311None of it is shown on the site. A question Jev judged unfit for the feed 312was always kept (`asked.listed`); now so is every question that ended any 313other way, in `events`. 314 315Visits are counted from a second cookie, `lmjtfy_visit`, which holds the 316time the browser was last counted: a page view compares it with now and 317marks itself the browser's first ever, first today, or first in half an 318hour. 319 320### The admin door 321 322The owner reads all of this from a separate, private Worker, which binds 323this Worker's archive object (`script_name` in its `wrangler.toml`) and 324posts `archive::Admin` messages to the object's `/admin` path: 325 326- `Select`: one SQL statement that only reads (`archive::reads_only`), and 327 its rows back. 328- `Moderate`: the owner's say on whether a question is on the feed, kept in 329 `asked.moderated` beside Jev's own verdict in `listed`. The feed shows 330 `COALESCE(moderated, listed)`. 331 332This Worker has no route that reaches that path. A visitor's request is 333passed to the object only as a socket upgrade on `/live`. 334 335### What Cloudflare keeps 336 337Cloudflare, which runs the site, keeps its own record, in the owner's 338account and nowhere public: 339 340- **Workers Logs**, for 3 days: one entry per request the Worker or its 341 objects handle, with the request as it arrived, address included. 342- **Traces**, for 7 days: each request's timing, through the fetches and 343 the Durable Object calls it made. 344- **Metrics**, the request counts and errors every Worker has. 345- **Web Analytics**: on `lmjtfy.fun`, Cloudflare adds its beacon script 346 (`static.cloudflareinsights.com/beacon.min.js`) to each page it serves to 347 a browser, and the browser reports page views and load times to 348 `lmjtfy.fun/cdn-cgi/rum`. It sets no cookie. The Worker's own HTML does 349 not contain the script: Cloudflare adds it on the way out, and only for 350 browsers, so `curl` does not see it. 351 352The owner turned on the logs and traces (2026-10-02) and Web Analytics with 353the domain (2026-10-03). They are configured in 354[wrangler.toml](wrangler.toml) under `[observability]`. 355 356## Credentials 357 358All from 1Password, none ever written to a file here. TypeSafe's API refuses 359browser origins anyway, so every Jev call goes through the Worker and the 360key never reaches a page. 361 362- **`LMJTFY_TYPESAFE_API_KEY`**, lmjtfy's own Jev key, is a Worker secret. 363 Under `wrangler dev` it comes from the process environment, which 364 `op-env-run` fills from the host's 1Password Environment. Deployed, it is 365 set with `lmjtfy-secret`. Without it the Worker still runs, and the page 366 says Jev is offline. 367- **The Cloudflare API token.** Workers AI has no local emulation, so even 368 `wrangler dev` calls the real models on the real account. `lmjtfy-wrangler` 369 (from the owner's devshell) reads the token from 1Password per run and 370 execs wrangler with it. 371- **`LMJTFY_GITHUB_TOKEN`**, a Worker secret: the clone proxy's GitHub 372 fine-grained token, Contents: read on `deizel/lmjtfy` and `deizel/jevcrates` 373 and no other permission. GitHub's API cannot mint a fine-grained token, so 374 it is made on GitHub and kept in the host's 1Password Environment, which 375 `op-env-run` hands to `wrangler dev`; deployed, it is set with 376 `op-env-run -- lmjtfy-secret github`. Without it a clone is answered with 503. 377- **`CLOUDFLARE_ANALYTICS_TOKEN`** and **`CLOUDFLARE_ACCOUNT_ID`**, Worker 378 secrets, are how the budget object reads the account's usage. The token can 379 read analytics and nothing else; nixos-config's infra declares it 380 (`cloudflare_account_token.lmjtfy-analytics`). The dev server runs without 381 them and counts only itself. 382 383## In this folder 384 385| Path | What | 386| --- | --- | 387| [src/](src/) | The Worker's code. Chapter 3 walks through it. | 388| [wrangler.toml](wrangler.toml) | The Worker's name, the pinned Jev model, the chosen LLM, the Jev budget, the bindings and the declared secrets. | 389| [build.rs](build.rs) | Stamps the build with the commit it came from (`LMJTFY_BUILD`). | 390| [Cargo.toml](Cargo.toml) | The crate: a `cdylib` for the Worker, and an `rlib` so its tests run natively. | 391 392`build/` and `.wrangler/` are the build's and the dev server's and are not 393committed. 394 395← Previous: [Chapter 1, apps/](../) · Up: [apps](../) · Next: [Chapter 3, the source](src/) →