Version: 1.0 Status: Specification Author: DBBasic Project Date: November 2025
Links: - PyPI: https://pypi.org/project/dbbasic-forum/ (pending) - GitHub: https://github.com/askrobots/dbbasic-forum (pending) - Specification: http://dbbasic.com/forum-spec
"Discussions are data. Store them like tables, query them like files."
Forums are persistent community discussions. Unlike sessions (ephemeral) or queues (transient work), forum content is permanent, searchable, and valuable. They belong in TSV storage.
1990s BBS Forums:
Flat files, one per message
/bbs/messages/001.txt
/bbs/messages/002.txt
Problems: No threading, no search, directory bloat
2000s vBulletin/phpBB:
CREATE TABLE threads (id, title, author, created_at);
CREATE TABLE posts (id, thread_id, content, parent_id, created_at);
Solution: Nested threading with parent_id Problem: Threads 15 levels deep, broken on mobile, arguments in tiny branches nobody sees
2010s Reddit/HackerNews:
Infinite nesting with "Continue this thread →"
Voting + collapse to manage depth
Solution: Voting surfaces good content Problem: Still breaks on mobile, conversations fragment, original topic lost
2015 Stack Overflow:
Question (top post)
Answers (flat list, no nesting)
Comments (lightweight, limited)
Solution: Flat threading, solution marking, stays on-topic Why it works: Every answer addresses the original question, can't derail into sub-arguments
2025 dbbasic-forum:
data/forum_categories.tsv
data/forum_threads.tsv
data/forum_posts.tsv (flat, no parent_id)
Solution: Stack Overflow model + TSV storage Trade-off: No nested arguments (feature, not bug)
Categories:
id name description order created_at
general General Discussion Talk about anything 0 2025-11-01T10:00:00
tech Technical Help Get help with tech 1 2025-11-01T10:01:00
Threads:
id category_id author_id title views replies created_at
t001 general u042 Welcome to the forum! 156 23 2025-11-01T11:00:00
t002 tech u123 How do I install Python? 89 12 2025-11-01T12:00:00
Posts:
id thread_id author_id content created_at
p001 t001 u042 Welcome everyone! 2025-11-01T11:00:00
p002 t001 u055 Thanks! Excited to be here 2025-11-01T11:15:00
Search all posts by user:
grep "author_id.*u042" data/forum_posts.tsv
Find unanswered questions:
awk -F'\t' '$6 == 0' data/forum_threads.tsv # replies column
Export for backup:
tar czf forum-backup.tar.gz data/forum*.tsv
git add data/ && git commit -m "Forum backup"
Migrate to PostgreSQL (when needed):
import csv
# TSV → SQL is trivial
forum_categories.tsv)id name description slug icon order is_active created_at updated_at
cat_001 General General discussion general 💬 0 true 2025-11-01T10:00:00 2025-11-01T10:00:00
cat_002 Technical Technical help technical 🔧 1 true 2025-11-01T10:01:00 2025-11-01T10:01:00
Columns:
- id: Unique category ID (cat_XXXXX)
- name: Display name
- description: Category description
- slug: URL-friendly identifier
- icon: Emoji or icon identifier
- order: Display order (0-based)
- is_active: Boolean (true/false)
- created_at: ISO 8601 timestamp
- updated_at: ISO 8601 timestamp
forum_threads.tsv)id category_id author_id title content ai_summary views is_pinned is_locked is_solved created_at updated_at
t001 cat_001 u042 Welcome! Welcome to our forum... AI: Welcoming... 156 true false false 2025-11-01T11:00:00 2025-11-01T11:00:00
t002 cat_002 u123 Python help How do I install... AI: Python install... 89 false false true 2025-11-01T12:00:00 2025-11-01T13:30:00
Columns:
- id: Unique thread ID (t_XXXXX)
- category_id: Foreign key to categories
- author_id: User ID from dbbasic-accounts
- title: Thread title
- content: Original post content
- ai_summary: AI-generated summary (optional)
- views: View count (integer)
- is_pinned: Pin to top (boolean)
- is_locked: Prevent new replies (boolean)
- is_solved: Marked as solved (boolean)
- created_at: ISO 8601 timestamp
- updated_at: ISO 8601 timestamp
forum_posts.tsv)id thread_id author_id content is_solution created_at updated_at
p001 t001 u042 Welcome everyone! false 2025-11-01T11:00:00 2025-11-01T11:00:00
p002 t001 u055 Thanks! @dan excited to be here false 2025-11-01T11:15:00 2025-11-01T11:15:00
p003 t002 u099 Try pip install python true 2025-11-01T13:30:00 2025-11-01T13:30:00
Columns:
- id: Unique post ID (p_XXXXX)
- thread_id: Foreign key to threads
- author_id: User ID from dbbasic-accounts
- content: Post content (Markdown supported, use @mentions or quotes for context)
- is_solution: Marked as solution (boolean)
- created_at: ISO 8601 timestamp
- updated_at: ISO 8601 timestamp
Note: No parent_id - flat threading only. Use @mentions or inline quotes for context.
from dbbasic_forum import Forum
forum = Forum(data_dir="data")
# Create category
cat_id = forum.create_category(
name="General Discussion",
description="Talk about anything",
slug="general",
icon="💬",
order=0
)
# List categories
categories = forum.list_categories() # Returns List[Dict]
# Get category
category = forum.get_category(cat_id) # Returns Dict or None
# Update category
forum.update_category(cat_id, name="New Name", order=1)
# Delete category
forum.delete_category(cat_id)
# Create thread
thread_id = forum.create_thread(
category_id="cat_001",
author_id="u042",
title="Welcome to the forum!",
content="Hello everyone, glad to be here..."
)
# List threads (with filtering)
threads = forum.list_threads(
category_id="cat_001", # Optional filter
author_id="u042", # Optional filter
is_pinned=True, # Optional filter
is_solved=False, # Optional filter
sort="recent", # recent|popular|trending
limit=20,
offset=0
)
# Get thread
thread = forum.get_thread(thread_id)
# Update thread
forum.update_thread(
thread_id,
title="Updated title",
content="Updated content",
is_pinned=True,
is_locked=False,
is_solved=True
)
# Increment view count
forum.increment_views(thread_id)
# Delete thread (and all posts)
forum.delete_thread(thread_id)
# Create post (reply)
post_id = forum.create_post(
thread_id="t001",
author_id="u055",
content="@dan Great thread! Looking forward to discussing..."
)
# List posts in thread (flat, sorted by time or votes)
posts = forum.list_posts(
thread_id="t001",
sort="recent" # or "oldest" or "votes"
)
# Get post
post = forum.get_post(post_id)
# Update post
forum.update_post(post_id, content="Edited content")
# Mark as solution
forum.mark_solution(post_id) # Also marks thread as solved
# Delete post
forum.delete_post(post_id)
# Search across threads and posts
results = forum.search(
query="python install",
search_in=["threads", "posts"], # or ["threads"] or ["posts"]
category_id="cat_002", # Optional filter
limit=20
)
# Get user's threads
threads = forum.get_user_threads(author_id="u042")
# Get user's posts
posts = forum.get_user_posts(author_id="u042")
# Get trending threads (activity in last 7 days)
trending = forum.get_trending_threads(days=7, limit=10)
# Get unanswered threads (no replies)
unanswered = forum.get_unanswered_threads(limit=10)
# Get solved threads
solved = forum.get_solved_threads(limit=10)
What happens with unlimited nesting:
Post: "How do I install Python?"
└─ Reply: "Use pip"
└─ Reply: "What's pip?"
└─ Reply: "Package manager"
└─ Reply: "But I need Python first"
└─ Reply: "That's circular"
└─ Reply: "No it's not" ← 7 levels deep, arguing, forgot original question
Mobile view: Scrolling sideways forever, unreadable
What's lost: Original question ("How do I install Python?") gets lost in nested arguments
Flat threading:
Question: "How do I install Python?"
Answer 1: "Download from python.org" ✓ (solution)
Answer 2: "Use homebrew: brew install python"
Answer 3: "Use pyenv for version management"
Benefits: - Every reply addresses the original question - Can't go off on tangents - Easy to scan - Mobile-friendly - Solution marking works perfectly - No tree traversal complexity
For context: Use @mentions or inline quotes:
@alice said: "What about Windows?"
For Windows, download the installer from python.org
Instead of nested replies, use:
1. @mentions:
@dan I tried that but got an error
2. Inline quotes:
> Use pip install
That gives me "pip not found"
3. Quote blocks (Markdown):
As @alice mentioned:
> Try the installer
That worked! Thanks.
No need for parent_id tree traversal. Keeps it simple.
from dbbasic_tsv import TSV
from datetime import datetime
import uuid
class Forum:
def __init__(self, data_dir="data"):
self.categories = TSV(
"forum_categories",
["id", "name", "description", "slug", "icon", "order",
"is_active", "created_at", "updated_at"],
data_dir=data_dir,
indexes=["id", "slug"]
)
self.threads = TSV(
"forum_threads",
["id", "category_id", "author_id", "title", "content",
"ai_summary", "views", "is_pinned", "is_locked",
"is_solved", "created_at", "updated_at"],
data_dir=data_dir,
indexes=["id", "category_id", "author_id"]
)
self.posts = TSV(
"forum_posts",
["id", "thread_id", "author_id", "content",
"is_solution", "created_at", "updated_at"],
data_dir=data_dir,
indexes=["id", "thread_id", "author_id"]
)
def create_thread(self, category_id, author_id, title, content):
thread_id = f"t_{uuid.uuid4().hex[:8]}"
now = datetime.utcnow().isoformat()
self.threads.insert({
"id": thread_id,
"category_id": category_id,
"author_id": author_id,
"title": title,
"content": content,
"ai_summary": "",
"views": 0,
"is_pinned": False,
"is_locked": False,
"is_solved": False,
"created_at": now,
"updated_at": now
})
return thread_id
def create_post(self, thread_id, author_id, content):
post_id = f"p_{uuid.uuid4().hex[:8]}"
now = datetime.utcnow().isoformat()
self.posts.insert({
"id": post_id,
"thread_id": thread_id,
"author_id": author_id,
"content": content,
"is_solution": False,
"created_at": now,
"updated_at": now
})
return post_id
def list_threads(self, category_id=None, sort="recent", limit=20, offset=0):
threads = self.threads.select()
# Filter by category
if category_id:
threads = [t for t in threads if t["category_id"] == category_id]
# Sort
if sort == "recent":
threads.sort(key=lambda t: t["created_at"], reverse=True)
elif sort == "popular":
threads.sort(key=lambda t: int(t["views"]), reverse=True)
# Pinned threads always first
pinned = [t for t in threads if t["is_pinned"] == "true"]
unpinned = [t for t in threads if t["is_pinned"] != "true"]
threads = pinned + unpinned
return threads[offset:offset+limit]
✅ Flat Threading: Single-level replies (like Stack Overflow) ✅ Categories: Organize discussions by topic ✅ Pinned Threads: Keep important threads at top ✅ Locked Threads: Prevent new replies ✅ Solution Marking: Stack Overflow-style solved status ✅ View Counting: Track thread popularity ✅ Search: grep-able TSV files ✅ Markdown Support: Rich text formatting with @mentions ✅ Quote Context: Inline quoting instead of threading
✅ AI Summaries: Optional AI-generated thread summaries ✅ Trending Algorithm: Activity-based sorting ✅ User History: All threads/posts by user ✅ Unanswered Detection: Find threads with no replies ✅ Permission Checks: Author/admin only edits ✅ Soft Deletes: Optional (mark deleted vs remove)
# api/forum/threads.py
from dbbasic_forum import Forum
from dbbasic_web.responses import html
from dbbasic_web.templating import render
def handle(request):
forum = Forum()
threads = forum.list_threads(
category_id=request.query.get("category"),
sort=request.query.get("sort", "recent")
)
return html(render("forum/threads.html", threads=threads))
from dbbasic_accounts import Accounts
from dbbasic_forum import Forum
accounts = Accounts()
forum = Forum()
# Create thread with user context
user = accounts.get_user(username="alice")
thread_id = forum.create_thread(
category_id="cat_001",
author_id=user["id"],
title="Hello from Alice",
content="My first post!"
)
# forum/admin.py
ADMIN_CONFIG = [{
'icon': '💬',
'label': 'Forum',
'href': '/admin/forum',
'order': 30,
'tables': ['forum_categories', 'forum_threads', 'forum_posts']
}]
Benchmarks (M1 MacBook Pro): - Create thread: <5ms - Create post: <3ms - List 20 threads: ~10ms - Search threads (grep): ~50ms for 10K threads - View thread + posts: ~15ms
Limits: - Categories: Unlimited (typically <50) - Threads per category: 100K recommended - Posts per thread: 10K recommended - Total posts: 1M maximum (then migrate to PostgreSQL)
When to migrate to PostgreSQL: - 1M+ total posts - Complex full-text search needed - Real-time notifications needed - Multiple servers (need shared state)
# Export from PostgreSQL
import csv
from dbbasic_forum import Forum
forum = Forum()
# Import categories
with open("categories.csv") as f:
for row in csv.DictReader(f):
forum.create_category(
name=row["name"],
description=row["description"],
slug=row["slug"]
)
# Import threads and posts
# Similar pattern...
# TSV → SQL is trivial
import csv
import psycopg2
conn = psycopg2.connect(database="forum")
with open("data/forum_threads.tsv") as f:
reader = csv.DictReader(f, delimiter="\t")
for row in reader:
conn.execute(
"INSERT INTO threads VALUES (%s, %s, %s, ...)",
(row["id"], row["category_id"], row["title"], ...)
)
# XSS Prevention
import html
content = html.escape(user_input)
# SQL Injection: N/A (no SQL)
# Path Traversal: N/A (no file operations)
def can_edit_thread(user_id, thread):
"""Only author or admin can edit"""
return user_id == thread["author_id"] or is_admin(user_id)
def can_lock_thread(user_id):
"""Only admin can lock"""
return is_admin(user_id)
def can_mark_solution(user_id, thread):
"""Only thread author or admin"""
return user_id == thread["author_id"] or is_admin(user_id)
# Prevent spam
from dbbasic_forum import Forum
forum = Forum()
# Check recent posts by user
recent_posts = forum.get_user_posts(
author_id=user_id,
since=datetime.utcnow() - timedelta(minutes=5)
)
if len(recent_posts) > 10:
raise RateLimitError("Too many posts in 5 minutes")
pip install dbbasic-forum[dev]
pytest tests/
Example tests:
def test_create_thread():
forum = Forum(data_dir="test_data")
thread_id = forum.create_thread(
category_id="cat_001",
author_id="u042",
title="Test Thread",
content="Test content"
)
assert thread_id.startswith("t_")
thread = forum.get_thread(thread_id)
assert thread["title"] == "Test Thread"
assert thread["views"] == 0
def test_flat_replies():
forum = Forum(data_dir="test_data")
thread_id = forum.create_thread(...)
post1_id = forum.create_post(thread_id, "u042", "First answer")
post2_id = forum.create_post(thread_id, "u055", "@dan Second answer")
posts = forum.list_posts(thread_id)
assert len(posts) == 2
# All posts at same level, no nesting
| Feature | dbbasic-forum | Discourse | phpBB | |
|---|---|---|---|---|
| Setup Time | 5 min | 2 hours | 1 hour | N/A (SaaS) |
| Dependencies | dbbasic-tsv | PostgreSQL, Redis, Sidekiq | MySQL, PHP | N/A |
| Lines of Code | ~300 | ~200K | ~150K | N/A |
| Storage | TSV files | PostgreSQL | MySQL | PostgreSQL |
| Search | grep | Elasticsearch | MySQL FTS | Elasticsearch |
| Real-time | SSE (optional) | WebSocket | Polling | WebSocket |
| Suitable For | <1M posts | Any size | <10M posts | Billions |
❌ Reddit-scale (millions of posts/day) ❌ Real-time chat (use websockets + Redis) ❌ Multi-server (needs shared state) ❌ Complex permissions (roles, moderators, etc.) ❌ Advanced search (fuzzy, typos, synonyms)
Rule of thumb: If you need full-text search with ranking + multiple servers, use PostgreSQL + Elasticsearch.
dbbasic-forum proves that discussion forums don't need PostgreSQL and Redis. For <1M posts, TSV storage is:
Perfect for: - Community sites: Discussion forums - Support sites: Q&A forums (Stack Overflow style) - Internal tools: Team discussions - Documentation: Inline discussions
Built from 18 years of production experience, distilled into 300 lines.
Remember: The best forum is one you actually ship.