← Back to Modules

dbbasic-forum Specification

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


Philosophy

"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.

Design Principles

  1. Foundation-First: Build on dbbasic-tsv
  2. Simple: ~200 lines of code
  3. Searchable: grep-able TSV files
  4. Flat Threading: Single-level replies, no nesting (like Stack Overflow)
  5. Feature-Complete: Categories, pinning, locking, solutions, AI summaries

Architecture Decision History

The Forum Evolution

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)


Why TSV for Forums

Forums Map Perfectly to Tables

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

What You Can Do With TSV Forums

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

Technical Specification

Storage Format

Categories Table (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

Threads Table (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

Posts Table (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.


API Specification

Forum Class

from dbbasic_forum import Forum

forum = Forum(data_dir="data")

Category Management

# 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)

Thread Management

# 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)

Post Management

# 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 and Filtering

# 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)

Why Flat Threading?

The Nesting Problem

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

The Stack Overflow Solution

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

When You Need Context

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.


Implementation

Core Implementation (~200 lines)

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]

Features

Core Features

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

Advanced Features

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)


Integration Examples

With dbbasic-web

# 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))

With dbbasic-accounts

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!"
)

With dbbasic-admin

# forum/admin.py
ADMIN_CONFIG = [{
    'icon': '💬',
    'label': 'Forum',
    'href': '/admin/forum',
    'order': 30,
    'tables': ['forum_categories', 'forum_threads', 'forum_posts']
}]

Performance

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)


Migration Path

From Discourse/Flarum

# 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...

To PostgreSQL (when needed)

# 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"], ...)
        )

Security

Input Validation

# XSS Prevention
import html
content = html.escape(user_input)

# SQL Injection: N/A (no SQL)

# Path Traversal: N/A (no file operations)

Permission Checks

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)

Rate Limiting

# 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")

Testing

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

Comparison

Feature dbbasic-forum Discourse phpBB Reddit
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

When NOT to Use

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.


Conclusion

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.