When I wrote about building this blog, comments were one of the few decisions with no obvious winner. I picked Giscus anyway. It was backed by GitHub Discussions, required no separate service to run, and fit the rest of the stack. I called out the one real drawback at the time: commenting requires a GitHub account. I said I was comfortable with that tradeoff and would revisit it if the friction ever became meaningful.
It became meaningful faster than I expected.
My readers are mostly developers, so having a GitHub account was rarely the problem. What actually stopped people was the OAuth screen itself.
“Act on your behalf” reads like Giscus can open issues or push commits as you, when all it actually wants is to read your public profile and post a discussion comment on my repo. That is just how GitHub’s generic OAuth scope screen is worded for any third-party app, but a reader skimming it before leaving a one-line comment has no reason to know that. More than one person told me they abandoned the comment after seeing that screen.
It turns out I wasn’t the only one who noticed. There is a long-running Giscus discussion about this exact wording, and it has been open for a while with no resolution in sight, since it isn’t really Giscus’s to fix. The problem wasn’t requiring a GitHub account. It was asking readers to grant permissions before they had even written a comment.
At that point, the tradeoff I had accepted in the earlier post no longer held. So I built the thing I had talked myself out of building the first time.
Building the replacement
The replacement stayed deliberately small. I wasn’t trying to build a full-fledged commenting platform. I just wanted a system that removed the sign-in friction while keeping the operational overhead close to zero.
The architecture is straightforward: Firebase Auth handles authentication, Firestore stores comments and reactions, and a Cloudflare Worker sits in front of Firestore for reads. Every comment thread is cached at the edge, and the cache is invalidated whenever someone posts (in that colo), so pages stay fast without serving stale discussions for long. The entire system now lives alongside the rest of this blog instead of inside a GitHub Discussions thread.
Rather than inventing another identity system, I leaned on providers people already use. Readers can sign in with Google, GitHub, X, Facebook, or a passwordless email link. GitHub remains an option, but without the confusing OAuth experience that originally pushed people away.
The interaction model is intentionally simple. Posts support anonymous emoji reactions, while comments allow one level of threaded replies and can be sorted by newest or by community reactions. It keeps discussions easy to follow without introducing the complexity of deeply nested conversations.
Moderation follows the same philosophy. Comments and reactions are soft deleted so authors can retract them without leaving broken threads behind, while moderator actions are enforced through Firestore security rules rather than relying on UI restrictions. The interface only exposes controls I have permission to use, but the real enforcement happens on the backend.
Knowing when someone comments
The first version shipped without any notifications for new comments. Giscus at least emailed me through GitHub notifications; my own system had nothing. So I added a digest, not a real-time alert, since a Cloudflare Cron Trigger polling Firestore a few times a day is simpler than a Firebase Blaze-only Cloud Function and fits this blog’s comment volume better than real-time notifications. That also keeps the entire system on Firebase’s free tier.
- Every new comment or reply across the site goes into a digest sent to me, three times a day
- A reader who leaves a top-level comment can check “notify me of replies” and gets their own digest, scoped to just that thread
Building it with Claude
I built this over a handful of sessions with Claude Code, starting from a design spec for the Firestore schema and security rules, then working outward: the Worker’s cache and invalidate routes, the client SDK wiring, the comment and reaction UI, the sign-in row. Later sessions were mostly incremental: adding X and Facebook as providers, fixing a stale-cache bug where already-loaded clients kept serving old comments, making the list scrollable once threads got long, cleaning up small UI states like reaction hover and pill counts, and most recently the notification digest above. Each session picked up cleanly from the last because the spec and the commit history carried the context forward, which made it feel like a steady iteration.
The earlier post said I would revisit the comments decision if the friction ever became meaningful. This was that revisit. Sometimes the right tradeoff stops being the right one. The important part is noticing when it does.