Why Roll Your Own Comments?

Hosted comment services are convenient, but they come with trade-offs. You don’t control the data, the pricing can be prohibitive as your site grows, and your customization options are limited. Running your own back end to manage comments is the alternative, but that brings server maintenance and scaling concerns.

Firebase occupies a middle ground. Cloud Firestore gives you a real database and full control over your data structure, while handling the back-end infrastructure for you. You interact with everything from the front end, making it a practical fit for static site generators like Gatsby. This walkthrough covers the core pieces: wiring up Firestore, querying comments for a post, handling form submissions, and locking down the database with security rules.

Project Structure and Comment Flow

The starter repository contains a bare-bones Gatsby blog where the comment function is stubbed out. A sample comment loads, and form submissions only log to the console. The repository has separate branches for the start and finish of each step, so you can track changes as you go.

Four components handle the comments feature:

  • blog-post.js — grabs the post slug from a GraphQL query.
  • Comments.js — orchestrates the list and loads the form.
  • CommentForm.js — renders the form and manages submission.
  • Comment.js — renders individual comments and reply toggles.

The slug is the unique identifier for each post. A Gatsby slug ends with a slash, so the code uses substring() to strip it before passing the value to the comments components. The system nests replies one level deep: a top-level comment can have replies, but those replies cannot have their own children.

For avatars, the project pulls images from the Adorable API, and Moment.js formats timestamps into readable relative time strings.

Setting Up Firebase and Firestore

Sign up for a Google account at Firebase, then create a new project via “Add Project.” Once the project exists, open the “Database” section from the left menu and click “Create database” to initialize Cloud Firestore. Choose “Start in test mode” during setup and pick the region closest to you.

Back in your application, install the Firebase client and create a firebase.js file in the root with your project configuration:

import firebase from "firebase/app"
import "firebase/firestore"

var firebaseConfig = 'yourFirebaseConfig'

firebase.initializeApp(firebaseConfig)

export const firestore = firebase.firestore()

export default firebase

To find your config, click the gear icon next to “Project Overview” on the Firebase console. Under your app section, select the web icon, register an app with a nickname, and copy the firebaseConfig object from the popup. Paste it into firebase.js, replacing the placeholder value.

On Exposing Your API Key

It is safe to expose the API key. Its purpose is to identify your project, not authorize access. A Google engineer has confirmed this. The real protection comes from security rules on your Firestore database. In the database’s default test mode, anyone with your URL can read and write data, so you must ship with strict rules before going public.

Data Modeling in Firestore

Cloud Firestore is a NoSQL database organized into collections and documents. A collection holds documents; each document holds field-value pairs. Collections cannot directly contain other collections — a document is required between them to nest data.

The natural impulse is to store comments in a deeply nested structure under a blog post collection:

blog/{blog-post-1}/content/comments/{comment-1}

However, deeply nested data files are harder to query and more error-prone. Firebase developers often recommend avoiding sub-collections. A flattened design keeps retrieval and writes simple.

For this project, you manually create sample data in the Firestore console. Create a collection called comments, use an auto-generated document ID, and match the field names and types shown in the screenshot. Each document corresponds to one comment.

Reading and Writing Comments

Firestore offers two read methods. get() fetches a snapshot once. onSnapshot() fetches data and continues to push updates until you detach the listener. The comments section relies on onSnapshot() so new comments appear without a page refresh.

Similarly, set() writes with a document ID you define, while add() lets Firestore generate IDs automatically. The comment form uses add() because the client does not need to manage unique keys.

In blog-post.js, import the Firestore instance and use useEffect() to subscribe to updates from the comments collection. The filter() and map() methods narrow results to those where the slug field matches the current post. Since onSnapshot() creates a persistent subscription, you must clean it up to prevent memory leaks — the listener function returned by onSnapshot() handles that:

useEffect(() => {
    const cleanUp = firestore
      .doc(`comments/${slug}`)
      .collection("comments")
      .onSnapshot(snapshot => {
        const posts = snapshot.docs.map(doc => {
          return { id: doc.id, ...doc.data() }
        })
        setComments(posts)
      })
    return () => cleanUp()
  }, [slug])

On the form side, import Firestore into CommentForm.js and replace the console-logging submission handler with an add() call. Get a reference to the comments collection and push the comment payload through the method, wrapping it with a catch() handler for error reporting.

With both ends connected, submit a form entry and you will find it stored in the Firestore console. New comments render automatically on the page through the active snapshot listener.

Securing the Database

Test mode requires zero authentication, which is only acceptable during development. Firestore security rules control access patterns at a fine-grained level. Read operations split into get (single document) and list (a collection of documents); writes split into create, update, and delete.

Applying targeted rules limits what visitors can do:

service cloud.firestore {
    match /databases/{database}/documents {
    match /comments/{id=**} {
        allow read, create;
    }
    }
}

This pattern declares rules for the comments collection, permitting anyone to read or create documents in it. If you instead allowed general write access, visitors could also update and delete existing comments, which is typically undesirable. Rules can be extended further to restrict specific document fields or to gate access behind user authentication.

Extending the Comment System Beyond the Basics

The implementation above demonstrates how Firebase handles real-time data flow with minimal server-side code. Security rules and Firestore’s built-in querying keep the system lightweight, while the SDK manages connection state and error handling for you.

From this foundation, the feature set can scale in several practical directions:

  • Attach profile images by uploading them to Cloud Storage for Firebase and storing the download URL on each comment document.
  • Enable user accounts and session management by integrating Firebase Authentication, then binding the uid to comment creation rules.
  • Rework the UI to support inline, Medium-style annotations where comments anchor to specific text selections rather than a single page-level thread.

For a deeper dive into querying, indexes, and offline persistence, the Firestore documentation is the recommended starting point. Security rule syntax and data modeling guidance there will cover your next iteration.

If you build on this pattern, note how the rule structure separates read access from write validation. That distinction is what allows anonymous browsing while still enforcing content constraints on insert. Adjust the request.resource.data checks if you add fields like moderation status or edit history.

Rather than wiring up a dedicated backend for every new interaction, the same collection-plus-rules approach handles comment threads, reaction summaries, and threaded replies without additional infrastructure.

What to Try Next

Turn the single-level comment list into a threaded discussion by adding a parentId field and recursively querying children. Alternatively, move the comment form into a modal component so the Firestore listener only mounts when the thread is visible.

Smashing Editorial

Share your own implementation notes below, especially if you’ve combined Firebase with a custom moderation workflow or a reactive front-end framework.