ADR-0022: Reader comments — guest comments held for moderation, D1 outside the graph, first-party proof of work, agents label and hide while people approve
- Status: Accepted
- Date: 2026-10-12
- Related: RFC-0007 (the design), ADR-0012 (Workers Cache, cache tags, purge through the site Worker), ADR-0014 (out-of-band approvals), ADR-0015 (agent modes), ADR-0018 (roles and scopes), ADR-0021 (site analytics: the pattern for visitor-facing, privacy-sensitive features)
- Sources: those listed in RFC-0007 (Cloudflare Workers Cache, purge limits, Turnstile, Rate Limiting, Workers AI, D1 limits and jurisdictions; EmDash, Ghost, WordPress, Substack, Isso, Remark42, ALTCHA; GDPR, DSA, KVKK, Law 5651); research
.internal/research/14-yorumlar.md(private); a live check on 2026-10-12 that Workers Cache servescf-cache-status: HITon the deployed workers.dev site
Context
Sites have no way for readers to respond. Comments are the first free-form visitor text Cloudhal stores and the first that reaches agents, so they bring spam, privacy and prompt-injection risk. Pages are rendered from immutable snapshots and served from the edge cache; D1’s Free daily write budget is shared with the CMS; the CSP is 'self'; agents act through MCP under the policy engine.
Decision
Adopt RFC-0007 with the owner’s decisions of 2026-10-12 and its reviewed defaults:
- Identity (MVP). Guest comments: a name and an optional private email; every guest comment is
pendinguntil a person approves it. Verified readers (email code or magic link through a second Better Auth instance with host-only cookies, Better Auth ≥ 1.7.7) follow in v1.1 as the base for members. - Storage. D1 tables in a new
@cloudhal/commentspackage (migration0016_comments), keyed by entry id, outside the snapshot graph: snapshots are immutable (erasure), and a content rollback must not touch readers’ comments. Settings per site, collection and entry live in D1. Comments are off by default; threads close 90 days after first publication. - Rendering. Approved comments are server-rendered into the cached article page and tagged
cloudhal-comments-<nodeId>; moderation purges that tag through the existing purge path, batched inside the shared purge budget (hides and takedowns first). A plain-HTML form posts to/__cloudhal/comments; a small first-party module under/_cloudhal/enhances it on open threads only. The script rule becomes “only first-party Cloudhal scripts under/_cloudhal/”. - Anti-spam before any D1 write: enabled and open thread, same origin, size, schema, honeypot with a fake “pending” answer, first-party proof of work (signed stateless challenges, single use; CSP stays
'self'), rate limits, content rules, a per-site daily cap, bans. Turnstile is opt-in per site and widens the CSP only on comment pages. Workers AI labels are opt-in and advisory. - Privacy. No IP stored, only a hash with a monthly salt; emails encrypted and visible to Editor+ only (person-only
commenters:data); pending and spam comments deleted after 30 days; erasure anonymises by default and may delete the text, audited; “Report comment” and statement-of-reasons records (DSA Art. 16/17). - Agents. Scopes
comments:read(default for new keys),comments:moderate(:own),comments:reply(:own). Agents may label and hide (reversible); approving comments, publishing replies and reopening threads wait for a person in every mode; no hard delete or bans over MCP. Comment text reaches agents only as labelled untrusted data with output caps; no tool acts on comment text. Comment approvals bind to the exact comments and their state, not the site-graph hash.
Alternatives considered
| Alternative | Pros | Cons | Why not |
|---|---|---|---|
| Third-party embed (Disqus, Hyvor) | No work | Tracking, data leaves the deployment, third-party script and iframe | Privacy and CSP |
| GitHub-backed (Giscus, Utterances) | No storage | Readers need GitHub; third-party iframe | Excludes most readers |
| Comments in the snapshot graph | One store, versioned | Erasure impossible in immutable snapshots; rollbacks would rewrite readers’ words; publish churn | Wrong lifecycle |
| Client-side rendering only | Simple purge story | No SEO, no JS-less reading, an extra request per view | SSR with tag purge works on our cache |
| Turnstile by default | Strong bot signal | Third-party script/iframe, CSP widening, privacy addendum | Opt-in instead |
Consequences
- Positive. Native, private, cache-friendly comments on every plan; moderation that agents can help with safely; DSA/GDPR/KVKK tooling built in.
- Negative. A moderation queue people must work; more D1 writes (~12–15 rows per comment over its life); purge budget shared with publishing; a second first-party script.
- Risks. Spam beyond proof of work (rate limits, caps, opt-in Turnstile/AI); prompt injection through comment text (untrusted rendering, caps, no comment-driven tools); purge budget exhaustion (queue, back-off; step-0 verification); the step-0 live checks on a Workers Free account are still open.