user@gryczka.dev: ~/garden/cloudflare-live-blog
$ cat ~/garden/cloudflare-live-blog/README.md
Live Blog — Newsroom Coverage on a Durable Object screenshot

Live Blog — Newsroom Coverage on a Durable Object

July 24, 2026 | Patrick Gryczka

## Overview

A rebuild of an earlier Next.js live blog, reworked to put the reader first. Each story is a single Durable Object addressed by name, so every author and reader worldwide lands on the same instance — which turns total ordering into an autoincrementing integer, fan-out into a loop over local sockets, and read-after-write into a non-problem. Posts live in the object's embedded SQLite database with two separate sequences: one fixes chronological order so a correction to an old post does not resurface it, and one advances on every mutation so a reconnecting reader replays creations, edits, and deletions in a single query. Astro reads the object over RPC during server rendering, placing the whole feed in the first HTML byte, and ships one plain-TypeScript island; Preact loads only on the author console, which readers never request. Authoring is gated by a capability token carried in the URL fragment, so it never reaches server logs, and only its SHA-256 digest is persisted. Abuse guardrails are split deliberately: Rate Limiting bindings reject junk at the edge keyed on the token digest rather than an IP, while authoritative totals are counted in SQL inside the object. A 180-line markdown renderer that escapes input before emitting any tags replaces DOMPurify, keeping jsdom out of the Durable Object entirely.

## Highlights

  • -> Reader JavaScript cut from 364,237 bytes to 20,200 by server-rendering the feed and shipping one framework-free island, with the budget enforced in CI
  • -> Two sequences per post: creation order keeps the feed stable while a mutation cursor lets one query replay creates, edits, and soft deletes to a reconnecting client
  • -> Capability tokens ride in the URL fragment so they never reach server logs or Referer headers, and only SHA-256 digests are persisted
  • -> Guardrails split by what each layer can actually guarantee — edge Rate Limiting keyed on token digest, authoritative totals counted in SQLite
  • -> A 180-line escape-then-format markdown renderer replaces DOMPurify, keeping jsdom out of the Durable Object, with tests asserting the closed output tag set
  • -> Strict CSP with no unsafe-inline or unsafe-eval, verified in-browser to still permit the same-origin WebSocket

## Architecture

Live Blog architecture diagram
Reader (19.7KB island)          Desk (Preact console)
    │   GET /blog/:id                │   writes + X-Edit-Token
    ▼                                ▼
Cloudflare Worker (src/worker.ts)
    ├── /api/blogs/* answered before Astro
    │      └── 101 upgrade must pass through untouched
    ├── Rate Limiting bindings ── keyed on token digest
    └── Astro SSR ── RPC getSnapshot(), no HTTP hop
            ▼
LiveBlog Durable Object (one per story)
    ├── capability check ──── SHA-256 digest, constant time
    ├── created_seq ───────── stable feed order
    ├── seq ───────────────── resume cursor for ?since=N
    ├── SQLite ────────────── posts + meta, authoritative counts
    ├── hibernatable sockets  presence + fan-out
    └── alarm ─────────────── 30-day idle cleanup
            ▼
Feed in the first HTML byte, then deltas over WSS

## Tech Stack

WorkersDurable ObjectsSQLiteWebSocketsRate LimitingAstroTypeScript
$ _