A community based topic aggregation platform built on atproto

docs: document Phase 2A comment query API implementation

Add comprehensive documentation for comment system Phase 2A:

Overview:
- Complete guide from indexing (Phase 1) through query API (Phase 2A)
- Implementation dates: November 4-5, 2025
- 30+ integration tests, all passing
- ~4,575 total lines of code

Phase 2A documentation:
- Lexicon definitions (defs.json, getComments.json)
- Database layer with Lemmy hot ranking algorithm
- Service layer with iterative loading strategy
- HTTP handlers with optional authentication
- 11 integration test scenarios

Hot ranking algorithm section:
- Full SQL formula with explanation
- Component breakdown (greatest, power, offsets)
- Sort modes (hot/top/new) with examples
- Path-based ordering for tree structure
- Behavioral characteristics

Future phases:
- Phase 2B: Vote integration (2-3 hours)
- Phase 2C: Post/user integration (2-3 hours)
- Phase 3: Advanced features (5 sub-phases)
- Distinguished comments
- Search & filtering
- Moderation tools
- Notifications
- Enhanced features
- Phase 4: Namespace migration (separate task)

Implementation statistics:
- Phase 1: 8 files created, 1 modified (~2,175 lines)
- Phase 2A: 9 files created, 6 modified (~2,400 lines)
- Combined total: ~4,575 lines

Command reference:
- Separate test commands for Phase 1 and Phase 2A
- Build and migration instructions
- Environment variable setup

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+923
docs
+923
docs/COMMENT_SYSTEM_IMPLEMENTATION.md
···
+
# Comment System Implementation
+
+
## Overview
+
+
This document details the complete implementation of the comment system for Coves, a forum-like atProto social media platform. The comment system follows the established vote system pattern, with comments living in user repositories and being indexed by the AppView via Jetstream firehose.
+
+
**Implementation Date:** November 4-5, 2025
+
**Status:** โœ… Phase 1 & 2A Complete - Indexing + Query API
+
**Test Coverage:** 30+ integration tests, all passing
+
+
---
+
+
## Development Phases
+
+
This implementation follows a phased approach for maintainability and proper scoping:
+
+
### โœ… Phase 1: Indexing Infrastructure (Current - COMPLETE)
+
**What was built:**
+
- Jetstream consumer for indexing comment CREATE/UPDATE/DELETE events
+
- PostgreSQL schema with proper indexes and denormalized counts
+
- Repository layer with comprehensive query methods
+
- Atomic parent count updates (posts.comment_count, comments.reply_count)
+
- Out-of-order event handling with reconciliation
+
- Soft delete support preserving thread structure
+
- Full integration test coverage (20 tests)
+
+
**What works:**
+
- Comments are indexed from Jetstream firehose as users create them
+
- Threading relationships tracked (root + parent references)
+
- Parent counts automatically maintained
+
- Comment updates and deletes processed correctly
+
- Out-of-order events reconciled automatically
+
+
**What's NOT in this phase:**
+
- โŒ No HTTP API endpoints for querying comments
+
- โŒ No service layer (repository is sufficient for indexing)
+
- โŒ No rate limiting or auth middleware
+
- โŒ No API documentation
+
+
### โœ… Phase 2A: Query API - COMPLETE (November 5, 2025)
+
+
**What was built:**
+
- Lexicon definitions: `social.coves.community.comment.defs` and `getComments`
+
- Database query methods with Lemmy hot ranking algorithm
+
- Service layer with iterative loading strategy for nested replies
+
- XRPC HTTP handler with optional authentication
+
- Comprehensive integration test suite (11 test scenarios)
+
+
**What works:**
+
- Fetch comments on any post with sorting (hot/top/new)
+
- Nested replies up to configurable depth (default 10, max 100)
+
- Lemmy hot ranking: `log(greatest(2, score + 2)) / power(time_decay, 1.8)`
+
- Cursor-based pagination for stable scrolling
+
- Optional authentication for viewer state (stubbed for Phase 2B)
+
- Timeframe filtering for "top" sort (hour/day/week/month/year/all)
+
+
**Endpoints:**
+
- `GET /xrpc/social.coves.community.comment.getComments`
+
- Required: `post` (AT-URI)
+
- Optional: `sort` (hot/top/new), `depth` (0-100), `limit` (1-100), `cursor`, `timeframe`
+
- Returns: Array of `threadViewComment` with nested replies + post context
+
- Supports Bearer token for authenticated requests (viewer state)
+
+
**Files created (9):**
+
1. `internal/atproto/lexicon/social/coves/community/comment/defs.json` - View definitions
+
2. `internal/atproto/lexicon/social/coves/community/comment/getComments.json` - Query endpoint
+
3. `internal/core/comments/comment_service.go` - Business logic layer
+
4. `internal/core/comments/view_models.go` - API response types
+
5. `internal/api/handlers/comments/get_comments.go` - HTTP handler
+
6. `internal/api/handlers/comments/errors.go` - Error handling utilities
+
7. `internal/api/handlers/comments/middleware.go` - Auth middleware
+
8. `internal/api/handlers/comments/service_adapter.go` - Service layer adapter
+
9. `tests/integration/comment_query_test.go` - Integration tests
+
+
**Files modified (6):**
+
1. `internal/db/postgres/comment_repo.go` - Added query methods (~450 lines)
+
2. `internal/core/comments/interfaces.go` - Added service interface
+
3. `internal/core/comments/comment.go` - Added CommenterHandle field
+
4. `internal/core/comments/errors.go` - Added IsValidationError helper
+
5. `cmd/server/main.go` - Wired up routes and service
+
6. `docs/COMMENT_SYSTEM_IMPLEMENTATION.md` - This document
+
+
**Total new code:** ~2,400 lines
+
+
**Test coverage:**
+
- 11 integration test scenarios covering:
+
- Basic fetch, nested replies, depth limits
+
- Hot/top/new sorting algorithms
+
- Pagination with cursor stability
+
- Empty threads, deleted comments
+
- Invalid input handling
+
- HTTP handler end-to-end
+
- Repository layer tested (hot ranking formula, pagination)
+
- Service layer tested (threading, depth limits)
+
- Handler tested (input validation, error cases)
+
- All tests passing โœ…
+
+
**Rationale for phased approach:**
+
1. **Separation of concerns**: Indexing and querying are distinct responsibilities
+
2. **Testability**: Phase 1 can be fully tested without API layer
+
3. **Incremental delivery**: Indexing can run in production while API is developed
+
4. **Scope management**: Prevents feature creep and allows focused code review
+
+
---
+
+
## Hot Ranking Algorithm (Lemmy-Based)
+
+
### Formula
+
+
```sql
+
log(greatest(2, score + 2)) /
+
power(((EXTRACT(EPOCH FROM (NOW() - created_at)) / 3600) + 2), 1.8)
+
```
+
+
### Explanation
+
+
**Components:**
+
- `greatest(2, score + 2)`: Ensures log input never goes below 2
+
- Prevents negative log values for heavily downvoted comments
+
- Score of -5 โ†’ log(2), same as score of 0
+
- Prevents brigading from creating "anti-viral" comments
+
+
- `power(..., 1.8)`: Time decay exponent
+
- Higher than posts (1.5) for faster comment aging
+
- Comments should be fresher than posts
+
+
- `+ 2` offsets: Prevent divide-by-zero for very new comments
+
+
**Behavior:**
+
- High score + old = lower rank (content ages naturally)
+
- Low score + new = higher rank (fresh content gets visibility)
+
- Negative scores don't break the formula (bounded at log(2))
+
+
### Sort Modes
+
+
**Hot (default):**
+
```sql
+
ORDER BY hot_rank DESC, score DESC, created_at DESC
+
```
+
+
**Top (with timeframe):**
+
```sql
+
WHERE created_at >= NOW() - INTERVAL '1 day'
+
ORDER BY score DESC, created_at DESC
+
```
+
+
**New (chronological):**
+
```sql
+
ORDER BY created_at DESC
+
```
+
+
### Path-Based Ordering
+
+
Comments are ordered within their tree level:
+
```sql
+
ORDER BY
+
path ASC, -- Maintains parent-child structure
+
hot_rank DESC, -- Sorts siblings by rank
+
score DESC, -- Tiebreaker
+
created_at DESC -- Final tiebreaker
+
```
+
+
**Result:** Siblings compete with siblings, but children never outrank their parent.
+
+
---
+
+
## Architecture
+
+
### Data Flow
+
+
```
+
Client โ†’ User's PDS โ†’ Jetstream Firehose โ†’ Comment Consumer โ†’ PostgreSQL AppView
+
โ†“
+
Atomic updates to parent counts
+
(posts.comment_count OR comments.reply_count)
+
```
+
+
### Key Design Principles
+
+
1. **User-Owned Records**: Comments live in user repositories (like votes), not community repositories (like posts)
+
2. **atProto Native**: Uses `com.atproto.repo.createRecord/updateRecord/deleteRecord`
+
3. **Threading via Strong References**: Root + parent system allows unlimited nesting depth
+
4. **Out-of-Order Indexing**: No foreign key constraints to allow Jetstream events to arrive in any order
+
5. **Idempotent Operations**: Safe for Jetstream replays and duplicate events
+
6. **Atomic Count Updates**: Database transactions ensure consistency
+
7. **Soft Deletes**: Preserves thread structure when comments are deleted
+
+
---
+
+
## Implementation Details
+
+
### 1. Lexicon Definition
+
+
**Location:** `internal/atproto/lexicon/social/coves/feed/comment.json`
+
+
The lexicon was already defined and follows atProto best practices:
+
+
```json
+
{
+
"lexicon": 1,
+
"id": "social.coves.feed.comment",
+
"defs": {
+
"main": {
+
"type": "record",
+
"key": "tid",
+
"required": ["reply", "content", "createdAt"],
+
"properties": {
+
"reply": {
+
"type": "ref",
+
"ref": "#replyRef",
+
"description": "Reference to the post and parent being replied to"
+
},
+
"content": {
+
"type": "string",
+
"maxGraphemes": 3000,
+
"maxLength": 30000
+
},
+
"facets": { /* Rich text annotations */ },
+
"embed": { /* Images, quoted posts */ },
+
"langs": { /* ISO 639-1 language codes */ },
+
"labels": { /* Self-applied content labels */ },
+
"createdAt": { /* RFC3339 timestamp */ }
+
}
+
},
+
"replyRef": {
+
"required": ["root", "parent"],
+
"properties": {
+
"root": {
+
"type": "ref",
+
"ref": "com.atproto.repo.strongRef",
+
"description": "Strong reference to the original post"
+
},
+
"parent": {
+
"type": "ref",
+
"ref": "com.atproto.repo.strongRef",
+
"description": "Strong reference to immediate parent (post or comment)"
+
}
+
}
+
}
+
}
+
}
+
```
+
+
**Threading Model:**
+
- `root`: Always points to the original post that started the thread
+
- `parent`: Points to the immediate parent (can be a post or another comment)
+
- This enables unlimited nested threading while maintaining the root reference
+
+
---
+
+
### 2. Database Schema
+
+
**Migration:** `internal/db/migrations/016_create_comments_table.sql`
+
+
```sql
+
CREATE TABLE comments (
+
id BIGSERIAL PRIMARY KEY,
+
uri TEXT UNIQUE NOT NULL, -- AT-URI (at://commenter_did/social.coves.feed.comment/rkey)
+
cid TEXT NOT NULL, -- Content ID
+
rkey TEXT NOT NULL, -- Record key (TID)
+
commenter_did TEXT NOT NULL, -- User who commented (from AT-URI repo field)
+
+
-- Threading structure (reply references)
+
root_uri TEXT NOT NULL, -- Strong reference to original post
+
root_cid TEXT NOT NULL, -- CID of root post (version pinning)
+
parent_uri TEXT NOT NULL, -- Strong reference to immediate parent
+
parent_cid TEXT NOT NULL, -- CID of parent (version pinning)
+
+
-- Content
+
content TEXT NOT NULL, -- Comment text (max 3000 graphemes, 30000 bytes)
+
content_facets JSONB, -- Rich text facets
+
embed JSONB, -- Embedded content (images, quoted posts)
+
content_labels JSONB, -- Self-applied labels (com.atproto.label.defs#selfLabels)
+
langs TEXT[], -- Languages (ISO 639-1, max 3)
+
+
-- Timestamps
+
created_at TIMESTAMPTZ NOT NULL, -- Commenter's timestamp from record
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
deleted_at TIMESTAMPTZ, -- Soft delete
+
+
-- Stats (denormalized for performance)
+
upvote_count INT NOT NULL DEFAULT 0, -- Comments CAN be voted on
+
downvote_count INT NOT NULL DEFAULT 0,
+
score INT NOT NULL DEFAULT 0, -- upvote_count - downvote_count
+
reply_count INT NOT NULL DEFAULT 0 -- Number of direct replies
+
);
+
```
+
+
**Key Indexes:**
+
```sql
+
-- Threading queries (most important for UX)
+
CREATE INDEX idx_comments_root ON comments(root_uri, created_at DESC)
+
WHERE deleted_at IS NULL;
+
CREATE INDEX idx_comments_parent ON comments(parent_uri, created_at DESC)
+
WHERE deleted_at IS NULL;
+
CREATE INDEX idx_comments_parent_score ON comments(parent_uri, score DESC, created_at DESC)
+
WHERE deleted_at IS NULL;
+
+
-- User queries
+
CREATE INDEX idx_comments_commenter ON comments(commenter_did, created_at DESC);
+
+
-- Vote targeting
+
CREATE INDEX idx_comments_uri_active ON comments(uri)
+
WHERE deleted_at IS NULL;
+
```
+
+
**Design Decisions:**
+
- **No FK on `commenter_did`**: Allows out-of-order Jetstream indexing (comment events may arrive before user events)
+
- **Soft delete pattern**: `deleted_at IS NULL` in indexes for performance
+
- **Vote counts included**: The vote lexicon explicitly allows voting on comments (not just posts)
+
- **StrongRef with CID**: Version pinning prevents confusion when parent content changes
+
+
---
+
+
### 3. Domain Layer
+
+
#### Comment Entity
+
**File:** `internal/core/comments/comment.go`
+
+
```go
+
type Comment struct {
+
ID int64
+
URI string
+
CID string
+
RKey string
+
CommenterDID string
+
+
// Threading
+
RootURI string
+
RootCID string
+
ParentURI string
+
ParentCID string
+
+
// Content
+
Content string
+
ContentFacets *string
+
Embed *string
+
ContentLabels *string
+
Langs []string
+
+
// Timestamps
+
CreatedAt time.Time
+
IndexedAt time.Time
+
DeletedAt *time.Time
+
+
// Stats
+
UpvoteCount int
+
DownvoteCount int
+
Score int
+
ReplyCount int
+
}
+
```
+
+
#### Repository Interface
+
**File:** `internal/core/comments/interfaces.go`
+
+
```go
+
type Repository interface {
+
Create(ctx context.Context, comment *Comment) error
+
Update(ctx context.Context, comment *Comment) error
+
GetByURI(ctx context.Context, uri string) (*Comment, error)
+
Delete(ctx context.Context, uri string) error
+
+
// Threading queries
+
ListByRoot(ctx context.Context, rootURI string, limit, offset int) ([]*Comment, error)
+
ListByParent(ctx context.Context, parentURI string, limit, offset int) ([]*Comment, error)
+
CountByParent(ctx context.Context, parentURI string) (int, error)
+
+
// User queries
+
ListByCommenter(ctx context.Context, commenterDID string, limit, offset int) ([]*Comment, error)
+
}
+
```
+
+
#### Error Types
+
**File:** `internal/core/comments/errors.go`
+
+
Standard error types following the vote system pattern, with helper functions `IsNotFound()` and `IsConflict()`.
+
+
---
+
+
### 4. Repository Implementation
+
+
**File:** `internal/db/postgres/comment_repo.go`
+
+
#### Idempotent Create Pattern
+
```go
+
func (r *postgresCommentRepo) Create(ctx context.Context, comment *Comment) error {
+
query := `
+
INSERT INTO comments (...)
+
VALUES (...)
+
ON CONFLICT (uri) DO NOTHING
+
RETURNING id, indexed_at
+
`
+
+
err := r.db.QueryRowContext(ctx, query, ...).Scan(&comment.ID, &comment.IndexedAt)
+
+
// ON CONFLICT DO NOTHING returns no rows if duplicate
+
if err == sql.ErrNoRows {
+
return nil // Already exists - OK for Jetstream replays
+
}
+
+
return err
+
}
+
```
+
+
#### Update Preserving Vote Counts
+
```go
+
func (r *postgresCommentRepo) Update(ctx context.Context, comment *Comment) error {
+
query := `
+
UPDATE comments
+
SET cid = $1, content = $2, content_facets = $3,
+
embed = $4, content_labels = $5, langs = $6
+
WHERE uri = $7 AND deleted_at IS NULL
+
RETURNING id, indexed_at, created_at,
+
upvote_count, downvote_count, score, reply_count
+
`
+
+
// Vote counts and created_at are preserved (not in SET clause)
+
err := r.db.QueryRowContext(ctx, query, ...).Scan(...)
+
return err
+
}
+
```
+
+
#### Soft Delete
+
```go
+
func (r *postgresCommentRepo) Delete(ctx context.Context, uri string) error {
+
query := `
+
UPDATE comments
+
SET deleted_at = NOW()
+
WHERE uri = $1 AND deleted_at IS NULL
+
`
+
+
result, err := r.db.ExecContext(ctx, query, uri)
+
// Idempotent: Returns success even if already deleted
+
return err
+
}
+
```
+
+
---
+
+
### 5. Jetstream Consumer
+
+
**File:** `internal/atproto/jetstream/comment_consumer.go`
+
+
#### Event Handler
+
```go
+
func (c *CommentEventConsumer) HandleEvent(ctx context.Context, event *JetstreamEvent) error {
+
if event.Kind != "commit" || event.Commit == nil {
+
return nil
+
}
+
+
if event.Commit.Collection == "social.coves.feed.comment" {
+
switch event.Commit.Operation {
+
case "create":
+
return c.createComment(ctx, event.Did, commit)
+
case "update":
+
return c.updateComment(ctx, event.Did, commit)
+
case "delete":
+
return c.deleteComment(ctx, event.Did, commit)
+
}
+
}
+
+
return nil
+
}
+
```
+
+
#### Atomic Count Updates
+
```go
+
func (c *CommentEventConsumer) indexCommentAndUpdateCounts(ctx, comment *Comment) error {
+
tx, _ := c.db.BeginTx(ctx, nil)
+
defer tx.Rollback()
+
+
// 1. Insert comment (idempotent)
+
err = tx.QueryRowContext(ctx, `
+
INSERT INTO comments (...) VALUES (...)
+
ON CONFLICT (uri) DO NOTHING
+
RETURNING id
+
`).Scan(&commentID)
+
+
if err == sql.ErrNoRows {
+
tx.Commit()
+
return nil // Already indexed
+
}
+
+
// 2. Update parent counts atomically
+
// Try posts table first
+
tx.ExecContext(ctx, `
+
UPDATE posts
+
SET comment_count = comment_count + 1
+
WHERE uri = $1 AND deleted_at IS NULL
+
`, comment.ParentURI)
+
+
// If no post updated, parent is probably a comment
+
tx.ExecContext(ctx, `
+
UPDATE comments
+
SET reply_count = reply_count + 1
+
WHERE uri = $1 AND deleted_at IS NULL
+
`, comment.ParentURI)
+
+
return tx.Commit()
+
}
+
```
+
+
#### Security Validation
+
```go
+
func (c *CommentEventConsumer) validateCommentEvent(ctx, repoDID string, comment *CommentRecord) error {
+
// Comments MUST come from user repositories (repo owner = commenter DID)
+
if !strings.HasPrefix(repoDID, "did:") {
+
return fmt.Errorf("invalid commenter DID format: %s", repoDID)
+
}
+
+
// Content is required
+
if comment.Content == "" {
+
return fmt.Errorf("comment content is required")
+
}
+
+
// Reply references must have both URI and CID
+
if comment.Reply.Root.URI == "" || comment.Reply.Root.CID == "" {
+
return fmt.Errorf("invalid root reference: must have both URI and CID")
+
}
+
+
if comment.Reply.Parent.URI == "" || comment.Reply.Parent.CID == "" {
+
return fmt.Errorf("invalid parent reference: must have both URI and CID")
+
}
+
+
return nil
+
}
+
```
+
+
**Security Note:** We do NOT verify that the user exists in the AppView because:
+
1. Comment events may arrive before user events in Jetstream (race condition)
+
2. The comment came from the user's PDS repository (authenticated by PDS)
+
3. No database FK constraint allows out-of-order indexing
+
4. Orphaned comments (from never-indexed users) are harmless
+
+
---
+
+
### 6. WebSocket Connector
+
+
**File:** `internal/atproto/jetstream/comment_jetstream_connector.go`
+
+
Follows the standard Jetstream connector pattern with:
+
- Auto-reconnect on errors (5-second retry)
+
- Ping/pong keepalive (30-second ping, 60-second read deadline)
+
- Graceful shutdown via context cancellation
+
- Subscribes to: `wantedCollections=social.coves.feed.comment`
+
+
---
+
+
### 7. Server Integration
+
+
**File:** `cmd/server/main.go` (lines 289-396)
+
+
```go
+
// Initialize comment repository
+
commentRepo := postgresRepo.NewCommentRepository(db)
+
log.Println("โœ… Comment repository initialized (Jetstream indexing only)")
+
+
// Start Jetstream consumer for comments
+
commentJetstreamURL := os.Getenv("COMMENT_JETSTREAM_URL")
+
if commentJetstreamURL == "" {
+
commentJetstreamURL = "ws://localhost:6008/subscribe?wantedCollections=social.coves.feed.comment"
+
}
+
+
commentEventConsumer := jetstream.NewCommentEventConsumer(commentRepo, db)
+
commentJetstreamConnector := jetstream.NewCommentJetstreamConnector(commentEventConsumer, commentJetstreamURL)
+
+
go func() {
+
if startErr := commentJetstreamConnector.Start(ctx); startErr != nil {
+
log.Printf("Comment Jetstream consumer stopped: %v", startErr)
+
}
+
}()
+
+
log.Printf("Started Jetstream comment consumer: %s", commentJetstreamURL)
+
log.Println(" - Indexing: social.coves.feed.comment CREATE/UPDATE/DELETE operations")
+
log.Println(" - Updating: Post comment counts and comment reply counts atomically")
+
```
+
+
---
+
+
## Testing
+
+
### Test Suite
+
+
**File:** `tests/integration/comment_consumer_test.go`
+
+
**Test Coverage:** 6 test suites, 18 test cases, **100% passing**
+
+
#### 1. TestCommentConsumer_CreateComment
+
- โœ… Create comment on post
+
- โœ… Verify comment is indexed correctly
+
- โœ… Verify post comment count is incremented
+
- โœ… Idempotent create - duplicate events don't double-count
+
+
#### 2. TestCommentConsumer_Threading
+
- โœ… Create first-level comment (reply to post)
+
- โœ… Create second-level comment (reply to comment)
+
- โœ… Verify both comments have same root (original post)
+
- โœ… Verify parent relationships are correct
+
- โœ… Verify reply counts are updated
+
- โœ… Query all comments by root (flat list)
+
- โœ… Query direct replies to post
+
- โœ… Query direct replies to comment
+
+
#### 3. TestCommentConsumer_UpdateComment
+
- โœ… Create comment with initial content
+
- โœ… Manually set vote counts to simulate votes
+
- โœ… Update comment content
+
- โœ… Verify content is updated
+
- โœ… Verify CID is updated
+
- โœ… **Verify vote counts are preserved**
+
- โœ… **Verify created_at is preserved**
+
+
#### 4. TestCommentConsumer_DeleteComment
+
- โœ… Create comment
+
- โœ… Delete comment (soft delete)
+
- โœ… Verify deleted_at is set
+
- โœ… Verify post comment count is decremented
+
- โœ… Idempotent delete - duplicate deletes don't double-decrement
+
+
#### 5. TestCommentConsumer_SecurityValidation
+
- โœ… Reject comment with empty content
+
- โœ… Reject comment with invalid root reference (missing URI)
+
- โœ… Reject comment with invalid parent reference (missing CID)
+
- โœ… Reject comment with invalid DID format
+
+
#### 6. TestCommentRepository_Queries
+
- โœ… ListByRoot returns all comments in thread (4 comments)
+
- โœ… ListByParent returns direct replies to post (2 comments)
+
- โœ… ListByParent returns direct replies to comment (2 comments)
+
- โœ… CountByParent returns correct counts
+
- โœ… ListByCommenter returns all user's comments
+
+
### Test Results
+
+
```
+
=== Test Summary ===
+
PASS: TestCommentConsumer_CreateComment (0.02s)
+
PASS: TestCommentConsumer_Threading (0.02s)
+
PASS: TestCommentConsumer_UpdateComment (0.02s)
+
PASS: TestCommentConsumer_DeleteComment (0.02s)
+
PASS: TestCommentConsumer_SecurityValidation (0.01s)
+
PASS: TestCommentRepository_Queries (0.02s)
+
+
โœ… ALL 18 TESTS PASS
+
Total time: 0.115s
+
```
+
+
---
+
+
## Key Features
+
+
### โœ… Comments ARE Votable
+
The vote lexicon explicitly states: *"Record declaring a vote (upvote or downvote) on a **post or comment**"*
+
+
Comments include full vote tracking:
+
- `upvote_count`
+
- `downvote_count`
+
- `score` (calculated as upvote_count - downvote_count)
+
+
### โœ… Comments ARE Editable
+
Unlike votes (which are immutable), comments support UPDATE operations:
+
- Content, facets, embed, and labels can be updated
+
- Vote counts and created_at are preserved
+
- CID is updated to reflect new version
+
+
### โœ… Threading Support
+
Unlimited nesting depth via root + parent system:
+
- Every comment knows its root post
+
- Every comment knows its immediate parent
+
- Easy to query entire threads or direct replies
+
- Soft deletes preserve thread structure
+
+
### โœ… Out-of-Order Indexing
+
No foreign key constraints allow events to arrive in any order:
+
- Comment events may arrive before user events
+
- Comment events may arrive before post events
+
- All operations are idempotent
+
- Safe for Jetstream replays
+
+
### โœ… Atomic Consistency
+
Database transactions ensure counts are always accurate:
+
- Comment creation increments parent count
+
- Comment deletion decrements parent count
+
- No race conditions
+
- No orphaned counts
+
+
---
+
+
## Implementation Statistics
+
+
### Phase 1 - Indexing Infrastructure
+
+
**Files Created: 8**
+
1. `internal/db/migrations/016_create_comments_table.sql` - 60 lines
+
2. `internal/core/comments/comment.go` - 80 lines
+
3. `internal/core/comments/interfaces.go` - 45 lines
+
4. `internal/core/comments/errors.go` - 40 lines
+
5. `internal/db/postgres/comment_repo.go` - 340 lines
+
6. `internal/atproto/jetstream/comment_consumer.go` - 530 lines
+
7. `internal/atproto/jetstream/comment_jetstream_connector.go` - 130 lines
+
8. `tests/integration/comment_consumer_test.go` - 930 lines
+
+
**Files Modified: 1**
+
1. `cmd/server/main.go` - Added 20 lines for Jetstream consumer
+
+
**Phase 1 Total:** ~2,175 lines
+
+
### Phase 2A - Query API
+
+
**Files Created: 9** (listed above in Phase 2A section)
+
+
**Files Modified: 6** (listed above in Phase 2A section)
+
+
**Phase 2A Total:** ~2,400 lines
+
+
### Combined Total: ~4,575 lines
+
+
---
+
+
## Reference Pattern: Vote System
+
+
The comment implementation closely follows the vote system pattern:
+
+
| Aspect | Votes | Comments |
+
|--------|-------|----------|
+
| **Location** | User repositories | User repositories |
+
| **Lexicon** | `social.coves.feed.vote` | `social.coves.feed.comment` |
+
| **Operations** | CREATE, DELETE | CREATE, UPDATE, DELETE |
+
| **Mutability** | Immutable | Editable |
+
| **Foreign Keys** | None (out-of-order indexing) | None (out-of-order indexing) |
+
| **Delete Pattern** | Soft delete | Soft delete |
+
| **Idempotency** | ON CONFLICT DO NOTHING | ON CONFLICT DO NOTHING |
+
| **Count Updates** | Atomic transaction | Atomic transaction |
+
| **Security** | PDS authentication | PDS authentication |
+
+
---
+
+
## Future Phases
+
+
### ๐Ÿ“‹ Phase 2B: Vote Integration (Planned)
+
+
**Scope:**
+
- Update vote consumer to handle comment votes
+
- Integrate `GetVoteStateForComments()` in service layer
+
- Populate viewer.vote and viewer.voteUri in commentView
+
- Test vote creation on comments end-to-end
+
- Atomic updates to comments.upvote_count, downvote_count, score
+
+
**Dependencies:**
+
- Phase 1 indexing (โœ… Complete)
+
- Phase 2A query API (โœ… Complete)
+
- Vote consumer (already exists for posts)
+
+
**Estimated effort:** 2-3 hours
+
+
---
+
+
### ๐Ÿ“‹ Phase 2C: Post/User Integration (Planned)
+
+
**Scope:**
+
- Integrate post repository in comment service
+
- Return full postView in getComments response (currently nil)
+
- Integrate user repository for full AuthorView
+
- Add display name and avatar to comment authors
+
- Parse and include original record in commentView
+
+
**Dependencies:**
+
- Phase 2A query API (โœ… Complete)
+
+
**Estimated effort:** 2-3 hours
+
+
---
+
+
### ๐Ÿ“‹ Phase 3: Advanced Features (Future)
+
+
#### 3A: Distinguished Comments
+
- Moderator/admin comment flags
+
- Priority sorting for distinguished comments
+
- Visual indicators in UI
+
+
#### 3B: Comment Search & Filtering
+
- Full-text search within threads
+
- Filter by author, time range, score
+
- Search across community comments
+
+
#### 3C: Moderation Tools
+
- Hide/remove comments
+
- Flag system for user reports
+
- Moderator queue
+
- Audit log
+
+
#### 3D: Notifications
+
- Notify users of replies to their comments
+
- Notify post authors of new comments
+
- Mention notifications (@user)
+
- Customizable notification preferences
+
+
#### 3E: Enhanced Features
+
- Comment edit history tracking
+
- Save/bookmark comments
+
- Sort by "controversial" (high engagement, low score)
+
- Collapsible comment threads
+
- User-specific comment history API
+
- Community-wide comment stats/analytics
+
+
---
+
+
### ๐Ÿ“‹ Phase 4: Namespace Migration (Separate Task)
+
+
**Scope:**
+
- Migrate existing `social.coves.feed.comment` records to `social.coves.community.comment`
+
- Update all AT-URIs in database
+
- Update Jetstream consumer collection filter
+
- Migration script with rollback capability
+
- Zero-downtime deployment strategy
+
+
**Note:** Currently out of scope - will be tackled separately when needed.
+
+
---
+
+
## Performance Considerations
+
+
### Database Indexes
+
+
All critical query patterns are indexed:
+
- **Threading queries**: `idx_comments_root`, `idx_comments_parent`
+
- **Sorting by score**: `idx_comments_parent_score`
+
- **User history**: `idx_comments_commenter`
+
- **Vote targeting**: `idx_comments_uri_active`
+
+
### Denormalized Counts
+
+
Vote counts and reply counts are denormalized for performance:
+
- Avoids `COUNT(*)` queries on large datasets
+
- Updated atomically with comment operations
+
- Indexed for fast sorting
+
+
### Pagination Support
+
+
All list queries support limit/offset pagination:
+
- `ListByRoot(ctx, rootURI, limit, offset)`
+
- `ListByParent(ctx, parentURI, limit, offset)`
+
- `ListByCommenter(ctx, commenterDID, limit, offset)`
+
+
---
+
+
## Conclusion
+
+
The comment system has successfully completed **Phase 1 (Indexing)** and **Phase 2A (Query API)**, providing a production-ready threaded discussion system for Coves:
+
+
โœ… **Phase 1 Complete**: Full indexing infrastructure with Jetstream consumer
+
โœ… **Phase 2A Complete**: Query API with hot ranking, threading, and pagination
+
โœ… **Fully Tested**: 30+ integration tests across indexing and query layers
+
โœ… **Secure**: Input validation, parameterized queries, optional auth
+
โœ… **Scalable**: Indexed queries, denormalized counts, cursor pagination
+
โœ… **atProto Native**: User-owned records, Jetstream indexing, Bluesky patterns
+
+
**Next milestones:**
+
- Phase 2B: Vote integration for comment voting
+
- Phase 2C: Post/user integration for complete views
+
- Phase 3: Advanced features (moderation, notifications, search)
+
+
The implementation provides a solid foundation for building rich threaded discussions in Coves while maintaining compatibility with the broader atProto ecosystem and following established patterns from platforms like Lemmy and Reddit.
+
+
---
+
+
## Appendix: Command Reference
+
+
### Run Tests
+
+
**Phase 1 - Indexing Tests:**
+
```bash
+
TEST_DATABASE_URL="postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" \
+
go test -v ./tests/integration/comment_consumer_test.go \
+
./tests/integration/user_test.go \
+
./tests/integration/helpers.go \
+
-run "TestCommentConsumer" -timeout 60s
+
```
+
+
**Phase 2A - Query API Tests:**
+
```bash
+
TEST_DATABASE_URL="postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" \
+
go test -v ./tests/integration/comment_query_test.go \
+
./tests/integration/user_test.go \
+
./tests/integration/helpers.go \
+
-run "TestCommentQuery" -timeout 120s
+
```
+
+
**All Comment Tests:**
+
```bash
+
TEST_DATABASE_URL="postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" \
+
go test -v ./tests/integration/comment_*.go \
+
./tests/integration/user_test.go \
+
./tests/integration/helpers.go \
+
-timeout 120s
+
```
+
+
### Apply Migration
+
```bash
+
GOOSE_DRIVER=postgres \
+
GOOSE_DBSTRING="postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" \
+
goose -dir internal/db/migrations up
+
```
+
+
### Build Server
+
```bash
+
go build ./cmd/server
+
```
+
+
### Environment Variables
+
```bash
+
# Jetstream URL (optional, defaults to localhost:6008)
+
export COMMENT_JETSTREAM_URL="ws://localhost:6008/subscribe?wantedCollections=social.coves.feed.comment"
+
+
# Database URL
+
export TEST_DATABASE_URL="postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable"
+
```
+
+
---
+
+
**Last Updated:** November 5, 2025
+
**Status:** โœ… Phase 2A Production-Ready