Building a Commenting System [Part 1]: Why Build Your Own?
I could have just embedded Disqus and called it a day. But I did what every normal developer does when they want to add a feature: roll my own commenting system from scratch.
The Easy Options
I looked at what's already out there:
Disqus is the obvious choice for a lot of people. Drop in a script tag, done. But it also comes with ads (unless you pay), tracking scripts, and a design that screams "third-party widget." Plus, I don't own the data.
Giscus and Utterances are easy to integrate. They use GitHub issues or discussions as the backend. Free, no ads, and the data lives in your repo. But they lock you into GitHub as the only auth provider. With nuxt-auth-utils, I can add Google, Discord, or any other OAuth provider just as easily. (Right now I'm using GitHub too, but the flexibility is there when I need it.)
Hyvor Talk (I actually like this one), and others are paid services. Some are quite nice, but I wasn't ready to commit to a monthly fee for a blog that might get 2 comments a month 😅.
Why Build Instead
A couple of reasons:
Data ownership. I want comments stored in my database, not scattered across third-party services. If I ever migrate platforms or the service shuts down, I don't want to lose years of discussions.
Design control. I wanted comments that look like they belong on this site, not an iframe with its own styling. The little details matter to me: typography, spacing, how actions feel when you click them etc. I'm not a designer by trade, but I've spent enough time on Figma to get it "close enough" and I know what bad design looks like.
Learning opportunity. I write about building fullstack Nuxt apps. What better way to explore patterns like real-time updates, optimistic UI, and moderation than to actually build something that needs them?
No tracking. I don't want to add third-party scripts that track my readers. Building my own means I know exactly what data is collected (just what's needed to show comments).
What We're Building
Here's the feature set I landed on:
- Nested threads with configurable depth limits (no infinite nesting)
- Reactions (thumbs up for now, extensible for more)
- Real-time updates via Server-Sent Events
- Optimistic UI that updates before the server responds
- Moderation tools for hiding, deleting, and managing reports
- Per-post settings to disable comments or reactions on specific posts
- OAuth login with GitHub (Google coming later)
Nothing revolutionary, but enough complexity to make it interesting.
The Architecture
If you've read my post on
Here's the high-level structure:
The flow is straightforward:
- Routes receive HTTP requests and delegate to controllers
- Controllers handle validation, auth, and orchestrate services
- Services execute database queries and business logic
- useComments composable manages client-side state and optimistic updates
- SSE broadcasts changes to all connected clients
The Tech Stack
- Database: SQLite with Drizzle ORM
- Auth: GitHub OAuth via nuxt-auth-utils
- Real-time: Server-Sent Events (no WebSocket complexity)
- Rich text: Tiptap editor
- Hosting: Cloudflare Workers + D1
I went with SQLite/D1 because that's what I'm already using for this site. The patterns would work just as well with Postgres or any other database.
What's Coming
This is the first post in a series where I'll dig into the interesting parts:
- Part 2: Designing the database schema for nested comments
- Part 3: Real-time updates with Server-Sent Events
- Part 4: Optimistic UI and the useComments composable
- Part 5: Moderation without the headaches
Each post will include real code from this site's codebase, not simplified examples.
Was It Worth It?
Honestly? It took longer than I expected. But I now have a commenting system that:
- Fits perfectly with the site's design
- Stores data in my own database
- Doesn't track my readers
- Taught me a bunch about real-time patterns
I'm thinking about polishing this into a standalone Nuxt module that anyone can drop into their site. The goal: bring your own CSS if you don't like the defaults, and everything else just works. If that sounds useful to you, let me know in the comments. 🥁
All Comments 0