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.
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 |
Based on your answers:
Path A: Zero Cost, Simple
Path B: Free Tier, Standard Features
Path C: Production Ready
Path D: Enterprise Grade
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
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
-- 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
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 => ({
'&': '&', '<': '<', '>': '>',
'"': '"', "'": '''
}[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>
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://...
Building your own comment system is a rewarding project that gives you:
Start simple, iterate based on real usage, and scale as needed. Good luck!
Navigation: