Chapter 14: Your Implementation Action Plan

This final chapter provides a step-by-step action plan to build and deploy your own comment system. Choose your path based on your needs and technical comfort level.

Decision Framework

Step 1: Assess Your Requirements

Answer these questions:

Question Options
Expected comments/month? <100, 100-1000, 1000-10000, 10000+
Budget for infrastructure? $0, <$25, <$100, unlimited
Moderation approach? None, post-mod, pre-mod, community
Authentication required? No, optional, required
Real-time updates needed? No, nice-to-have, essential

Step 2: Choose Your Stack

Based on your answers:

Path A: Zero Cost, Simple

Path B: Free Tier, Standard Features

Path C: Production Ready

Path D: Enterprise Grade

Path A: Zero Cost Implementation

Week 1: Core Setup

Day 1-2: Cloudflare Setup

# Install Wrangler
npm install -g wrangler

# Login to Cloudflare
wrangler login

# Create project
mkdir my-comments && cd my-comments
wrangler init

# Create D1 database
wrangler d1 create comments-db

Day 3-4: Database Schema

-- schema.sql
CREATE TABLE comments (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    page_id TEXT NOT NULL,
    parent_id INTEGER,
    author_name TEXT NOT NULL,
    content TEXT NOT NULL,
    status TEXT DEFAULT 'approved',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_page ON comments(page_id);
# Apply schema
wrangler d1 execute comments-db --file=schema.sql

Day 5-7: API Implementation

// src/index.js
export default {
    async fetch(request, env) {
        const url = new URL(request.url);
        
        // CORS headers
        const corsHeaders = {
            'Access-Control-Allow-Origin': 'https://yourdomain.com',
            'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
            'Access-Control-Allow-Headers': 'Content-Type',
        };
        
        if (request.method === 'OPTIONS') {
            return new Response(null, { headers: corsHeaders });
        }
        
        if (url.pathname === '/api/comments' && request.method === 'GET') {
            const pageId = url.searchParams.get('page_id');
            const { results } = await env.DB.prepare(
                'SELECT * FROM comments WHERE page_id = ? AND status = ? ORDER BY created_at DESC'
            ).bind(pageId, 'approved').all();
            
            return Response.json(results, { headers: corsHeaders });
        }
        
        if (url.pathname === '/api/comments' && request.method === 'POST') {
            const body = await request.json();
            
            await env.DB.prepare(
                'INSERT INTO comments (page_id, parent_id, author_name, content) VALUES (?, ?, ?, ?)'
            ).bind(body.page_id, body.parent_id, body.author_name, body.content).run();
            
            return Response.json({ success: true }, { status: 201, headers: corsHeaders });
        }
        
        return new Response('Not Found', { status: 404 });
    }
};

Deploy:

wrangler deploy

Week 1 Checklist


Path B: Standard Implementation

Week 1: Backend Setup

Day 1: Project Initialization

# Create project
mkdir comment-system && cd comment-system
python -m venv venv
source venv/bin/activate

# Install dependencies
pip install fastapi uvicorn supabase python-dotenv pydantic

# Create structure
mkdir -p app/{routes,services,models}
touch app/__init__.py app/main.py app/config.py

Day 2-3: Supabase Setup

  1. Create Supabase project at supabase.com
  2. Run SQL in Supabase SQL editor:
-- Tables
CREATE TABLE comments (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    page_id TEXT NOT NULL,
    parent_id UUID REFERENCES comments(id),
    author_name TEXT NOT NULL,
    author_email TEXT,
    content TEXT NOT NULL,
    status TEXT DEFAULT 'pending',
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_comments_page ON comments(page_id, status);

-- RLS
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Public read approved" ON comments
    FOR SELECT USING (status = 'approved');

CREATE POLICY "Anyone can insert" ON comments
    FOR INSERT WITH CHECK (true);

Day 4-5: FastAPI Implementation

# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from supabase import create_client
import os

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=[os.environ["ALLOWED_ORIGIN"]],
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
)

supabase = create_client(
    os.environ["SUPABASE_URL"],
    os.environ["SUPABASE_KEY"]
)

@app.get("/api/comments")
async def get_comments(page_id: str):
    response = supabase.table("comments") \
        .select("*") \
        .eq("page_id", page_id) \
        .eq("status", "approved") \
        .order("created_at", desc=True) \
        .execute()
    return {"data": response.data}

@app.post("/api/comments")
async def create_comment(data: dict):
    response = supabase.table("comments").insert(data).execute()
    return {"data": response.data[0]}

Day 6-7: Vercel Deployment

// vercel.json
{
    "builds": [
        {"src": "app/main.py", "use": "@vercel/python"}
    ],
    "routes": [
        {"src": "/api/(.*)", "dest": "app/main.py"}
    ]
}
# Deploy
vercel --prod

Week 2: Frontend Widget

Day 1-3: Widget Development

// public/widget.js
class CommentWidget {
    constructor(element) {
        this.el = element;
        this.apiUrl = element.dataset.api || '/api';
        this.pageId = element.dataset.pageId || window.location.pathname;
        this.init();
    }
    
    async init() {
        this.render();
        await this.loadComments();
        this.bindEvents();
    }
    
    render() {
        this.el.innerHTML = `
            <div class="comments-widget">
                <h3>Comments</h3>
                <form class="comment-form">
                    <input name="author_name" placeholder="Your name" required>
                    <textarea name="content" placeholder="Your comment" required></textarea>
                    <button type="submit">Post Comment</button>
                </form>
                <div class="comments-list"></div>
            </div>
        `;
    }
    
    async loadComments() {
        const res = await fetch(`${this.apiUrl}/comments?page_id=${this.pageId}`);
        const { data } = await res.json();
        
        const list = this.el.querySelector('.comments-list');
        list.innerHTML = data.length 
            ? data.map(c => this.commentHTML(c)).join('')
            : '<p>No comments yet.</p>';
    }
    
    commentHTML(c) {
        return `
            <article class="comment">
                <strong>${this.escape(c.author_name)}</strong>
                <time>${new Date(c.created_at).toLocaleDateString()}</time>
                <p>${this.escape(c.content)}</p>
            </article>
        `;
    }
    
    escape(str) {
        return str.replace(/[&<>"']/g, c => ({
            '&': '&amp;', '<': '&lt;', '>': '&gt;', 
            '"': '&quot;', "'": '&#39;'
        }[c]));
    }
    
    bindEvents() {
        const form = this.el.querySelector('form');
        form.addEventListener('submit', async (e) => {
            e.preventDefault();
            const data = Object.fromEntries(new FormData(form));
            data.page_id = this.pageId;
            
            await fetch(`${this.apiUrl}/comments`, {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(data)
            });
            
            form.reset();
            alert('Comment submitted for review!');
        });
    }
}

// Auto-init
document.querySelectorAll('[data-comments]').forEach(el => new CommentWidget(el));

Day 4-5: Admin Panel

Create a simple admin page at /admin:

<!-- admin.html -->
<!DOCTYPE html>
<html>
<head><title>Comment Moderation</title></head>
<body>
    <h1>Pending Comments</h1>
    <div id="queue"></div>
    
    <script>
        const API = '/api';
        
        async function loadQueue() {
            const res = await fetch(`${API}/admin/queue`, {
                headers: {'Authorization': `Bearer ${localStorage.getItem('token')}`}
            });
            const { data } = await res.json();
            
            document.getElementById('queue').innerHTML = data.map(c => `
                <div class="comment-card">
                    <p><strong>${c.author_name}</strong>: ${c.content}</p>
                    <button onclick="moderate('${c.id}', 'approved')">Approve</button>
                    <button onclick="moderate('${c.id}', 'rejected')">Reject</button>
                </div>
            `).join('');
        }
        
        async function moderate(id, status) {
            await fetch(`${API}/admin/comments/${id}`, {
                method: 'PATCH',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${localStorage.getItem('token')}`
                },
                body: JSON.stringify({ status })
            });
            loadQueue();
        }
        
        loadQueue();
    </script>
</body>
</html>

Week 2 Checklist


Path C: Production Implementation

Week 1-2: Infrastructure

Day 1-2: Fly.io Setup

# Install flyctl
curl -L https://fly.io/install.sh | sh

# Login and create app
fly auth login
fly launch --name my-comments-api

# Create PostgreSQL
fly postgres create --name my-comments-db

# Attach database
fly postgres attach my-comments-db

Day 3-5: Full API Development

Implement all features from chapters 4-8:

Day 6-7: Redis Setup

# Create Upstash Redis
# Via upstash.com dashboard

# Add to secrets
fly secrets set REDIS_URL=redis://...

Week 3: Advanced Features

Week 4: Testing & Launch


Launch Checklist

Pre-Launch

Launch Day

Post-Launch


Maintenance Schedule

Daily

Weekly

Monthly

Quarterly


Resources

Documentation

Open Source References

Community


Final Words

Building your own comment system is a rewarding project that gives you:

  1. Full control over your data
  2. Cost savings compared to commercial solutions
  3. Learning experience across the full stack
  4. Customization for your exact needs

Start simple, iterate based on real usage, and scale as needed. Good luck!


Navigation: