The console already knew how to hide and dismiss. It did not know that something had just happened unless I hit Refresh and read a full dump. This version is that sentence: what became new.
Opening a post is still not a Worker request. The public comment list is still { comments }. A clean post still publishes.
The expensive mistakes we did not take
The free tier is the design constraint. A doorbell that bills like a product is worse than no doorbell.
| Temptation | Why not |
|---|---|
| Poll the dump | One operator. A timer would spend Worker requests and D1 reads all day for nothing. Refresh stays the only dump hit. |
cache.enabled on the default entrypoint |
A cache HIT still counts as a Worker request. Turning it on would bill static posts. |
| Count views on GET | comment_views is the chart. A COUNT(*) on the hot path is not a knock. |
Treat an asset .json as the inbox |
Free to GET, immutable at runtime. Mutation is a deploy. Not an inbox. |
| R2 or KV as the mutable object | Parked. They were not earned. |
| Agents / Durable Object for Web Push | Crypto in the consumer is enough. A second runtime is not. |
| Emit dummy queue messages to “spend” 10k | Unused quota is not a bill. TTL exists so a quiet day still notifies, not so the bucket empties. |
The last version said Queue was not earned. The doorbell earned one.
State is not the inbox
comments, comment_views, and comment_flags are state. The inbox is a list of deltas that crossed a rule. Not every write becomes a row. Charts still come from the dump. The knock is approximate on purpose.
Three public mutators already write state. Each may increment a producer window:
| Request | State it already writes | Knock |
|---|---|---|
GET /api/comments |
one view row | views |
POST /api/comments |
the comment | comments, or held if policy hid it |
POST /api/comments/:id/flag |
a reader flag | flags, when the open count crosses a watermark |
A commenter ticks two streams on two requests. That is intended.
Cheap on the request, expensive on the Queue
The public path stays inside the 10 ms CPU habit. The producer is a Cache increment (the same store as the rate limits) and, rarely, queue.send. Views coalesce: default ten, or five minutes, whichever comes first. Comments and holds knock once. Flags knock when floor(count / 10) moves.
The consumer owns the cost: insert an inbox row, then Web Push if I turned it on. Fail open — a dead push does not roll back the row. Two raced messages insert once (id is the primary key).
System holds the cheap knobs: N per stream, T_send, push on or off. The expensive knobs — batch size, batch timeout, 24-hour retention — stay in the deploy so a slider cannot lie.
Push is a fan-out, not a second system
The inbox is the control surface. A toast is a copy of a row that already exists. Subscriptions live in D1, not on the dump. The service worker is /admin/sw.js, scoped to /admin/. Nothing registers on /.
Push stays off until I set VAPID secrets and flip the System toggle. Inbox still fills.
What you might notice
Almost nothing, if you are reading. Flag still says Flagged, not a raw 409. A failed post says Couldn’t post. A duplicate says Already sent.
If I am at the console: Inbox lists the knocks. Refresh, not a poll. A comment knocks immediately. Ten consented views on a quiet post knock once.
What did not change
Posts stay static. Two public comment endpoints, plus the small flag POST. Consent still gates the first fetch. Email is still never in the list. Hold-all and Workers AI stay off.
Search, a PWA, media uploads, a site-wide view counter, KV, and R2 stay named absences.
Comments
Comments load when this section is visible.
To load comments we set a first-party session cookie and record that you opened this section. Name and email stay in this browser after you post.