Published on
7 min read

Building a modern comment system: From database design to serverless deployment

Authors
  • avatar
    Name
    Liliana Summers
    Twitter

Remember when the internet was just a bunch of GeoCities pages with guestbooks? Those early attempts at community engagement have evolved into complex commenting systems, but somewhere along the way, we seem to have lost control over our own data and user experience.

As someone who values both user autonomy and data ownership (and gets a bit twitchy when services want me to "just trust them" with my users' data), I decided to build my own comment system for this blog. Sure, I could have used Giscus or Disqus, but where's the fun in that? Plus, I have some Opinions™ about third-party services controlling user interactions.

Blog Comment

Why Not Just Use Existing Solutions?

Let's address the elephant in the room - why build a custom comment system when solutions like Giscus and Disqus exist? Well:

  • Giscus requires users to have GitHub accounts - not everyone wants to tie their online identity to their professional GitHub profile just to comment on a post about gaming on Linux
  • Giscus requires your that your repo is public
  • Disqus is... let's be real, it's become the Internet Explorer of comment systems: bloated, privacy-invasive, and somehow still everywhere. The free tier serves ads. Gross.
  • All the above solutions mean giving up control over the user experience and data

The Tech Stack: A Love Letter to Modern Web Development

For this project, I chose:

  • Next.js 14 with App Router (because I like living on the edge)
  • Supabase for the database and authentication (because PostgreSQL is life)
  • Vercel for hosting (because serverless deployments spark joy)

Database Design: Keep It Simple, Keep It Secure

Here's where it gets interesting. I designed the database schema to be both simple and secure:

CREATE TABLE public.users (
  id UUID PRIMARY KEY,
  raw_user_meta_data JSONB
);

CREATE TABLE public.comments (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID REFERENCES public.users(id),
  post_slug TEXT NOT NULL,
  content TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

The beauty of this setup is its simplicity. Each comment is tied to both a user and a specific blog post via the post_slug, while keeping user metadata flexible through JSONB storage.

Authentication: Because Email Still Rules

While OAuth with GitHub/Google/etc. is trendy, I opted for good old email authentication with a twist - One-Time Passwords (OTP). Why? Because:

  1. Everyone has an email
  2. No password to remember
  3. No third-party authentication service required

Plus, it's just more accessible. Not everyone has (or wants) a GitHub account, but everyone reading a blog has an email address.

Blog Comment OTP OTP email OTP success

The Real Magic: Row Level Security

Supabase's Row Level Security (RLS) policies make it dead simple to implement proper security:

-- Allow anyone to read comments
CREATE POLICY "Anyone can read comments" ON comments FOR SELECT
  USING (true);

-- Only authenticated users can post
CREATE POLICY "Users can create comments" ON comments FOR INSERT
  WITH CHECK (auth.uid() = user_id);

-- Users can only edit their own comments
CREATE POLICY "Users can update own comments" ON comments FOR UPDATE
  USING (auth.uid() = user_id);
An approved comment

As you can see, the authenticated user can also delete their own comment

An deleted comment

Beyond Basic Comments: Threading and Dynamic Counts

Because who doesn't love a good nested discussion? I added comment threading and dynamic comment counts because:

  1. Threading allows for proper conversations instead of a flat wall of text
  2. Comment counts give readers a quick preview of the discussion activity and encourage participation

Comment Threading

The threading system was implemented using a self-referential structure in the comments table:

ALTER TABLE comments ADD COLUMN parent_id UUID REFERENCES comments(id);

This simple addition allows for parent comment nesting (we limit it to parent comments in order to prevent the dreaded "Reddit effect" where conversations become single-character-width columns).

The real magic happens in the comment fetching logic:

const commentTree = commentsWithNames.reduce((acc, comment) => {
  if (!comment.parent_id) {
    acc.push({ ...comment, replies: [] })
  } else {
    const parent = acc.find((c) => c.id === comment.parent_id)
    if (parent) {
      parent.replies = parent.replies || []
      parent.replies.push(comment)
    }
  }
  return acc
}, [])

This transforms our flat database structure into a nested tree of comments and replies - much like how this very blog post is structured with headings and subheadings.

A threaded comment

Dynamic Comment Counts

I added a little shake animation to the comment count icon too, because who doesn't love a bit of playful UI? The little shake animation every few seconds serves as a subtle reminder that "hey, there's a discussion happening down here!"

const CommentCount = ({ slug }: { slug: string }) => {
  const [count, setCount] = useState(null)
  const [shake, setShake] = useState(false)

  // Fetch comment count and handle shaking animation
  // ...
}
Comment count

The count is fetched via a serverless API route that queries Supabase directly:

const { count } = await supabase
  .from('comments')
  .select('*', { count: 'exact', head: true })
  .eq('post_slug', slug)
  .eq('status', 'approved')

This gives us real-time comment counts without having to rebuild the entire blog for each new comment.

Deploying to the Edge

The whole system is deployed on Vercel's edge network, meaning comments load faster than you can say "but what about WordPress comments?" The serverless architecture means:

  • No servers to maintain
  • Automatic scaling
  • Global distribution
  • Cost-effective (free for my usage!)

The Result?

A lightweight, fast, secure comment system that:

  • Respects user privacy
  • Gives me full control over the data
  • Costs basically nothing to run
  • Doesn't require users to create yet another account or link their GitHub

The best part? The entire system is simpler than trying to debug why Disqus is causing layout shifts on mobile devices.

What's Next?

I'm considering adding features like:

  • Markdown support for comments
  • Reactions (because sometimes a quick "lol" is all you need)
  • Email notifications for replies

But for now, I'm just happy to have a comment system that doesn't make me feel like I'm selling my users' data to the highest bidder.

Want to see the system in action? Leave a comment below! (Yes, that's a bit meta, but how could I resist?)

TLDR

I built my own comment system for this blog instead of using existing solutions like Disqus because:

  • I wanted to keep your data private and secure
  • You shouldn't need a GitHub account just to leave a comment
  • I wanted the system to be as simple as possible - just enter your email, get a one-time code, and you're ready to comment
  • The whole thing runs faster than traditional comment systems because it uses modern technology

Think of it like a guestbook for each blog post, but one that:

  • Doesn't track you across the internet
  • Doesn't sell your data to advertisers
  • Doesn't require you to remember another password
  • Just works™

Plus, building it myself means I can add fun features later based on what you all actually want to use, not what some corporate algorithm thinks will maximize "engagement".


Now excuse me while I go write more blog posts about gaming on Linux, because apparently that's my thing now. 🎮 🐧

Comments

You must be signed in to comment or reply.

It's very quiet here 👻

Join the Discussion

To contribute to the discussion, we need to verify your email. We'll send you a one-time password to confirm your address. Once verified, you'll be able to post comments.

Only your name will be displayed with your comments and can be updated each time you sign in.