A community based topic aggregation platform built on atproto

docs(prd): update PRDs to reflect Alpha post creation completion

Update PRD_POSTS.md with implementation status:
- Add "Implementation Status" section showing completed work
- Mark Alpha CREATE features as complete (✅)
- Mark Beta features as deferred (⚠️)
- Update all sections with checkmarks and status
- Add database schema status (migration 011 complete)
- Update success metrics (Alpha checklist complete)
- Reference IMPLEMENTATION_POST_CREATION.md for details

Completed (Alpha):
✅ Post creation endpoint with write-forward to community PDS
✅ Handler with authentication, validation, security checks
✅ Service layer with token refresh and community resolution
✅ PostgreSQL repository with proper indexing
✅ Jetstream consumer for real-time indexing
✅ E2E tests (service, repository, handler, live PDS+Jetstream)
✅ All 4 at-identifier formats supported

Deferred (Beta):
⚠️ Content rules validation
⚠️ Post read operations (get, list)
⚠️ Post update/edit operations
⚠️ Post deletion
⚠️ Voting system

Update other PRDs:
- PRD_BACKLOG: Add post creation to completed items
- PRD_COMMUNITIES: Reference post integration
- PRD_GOVERNANCE: Note content rules deferred to Beta

PRDs now accurately reflect codebase state.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

+92
docs/PRD_BACKLOG.md
···
## 🟡 P1: Important (Alpha Blockers)
### did:web Domain Verification & hostedByDID Auto-Population
**Added:** 2025-10-11 | **Updated:** 2025-10-16 | **Effort:** 2-3 days | **Priority:** ALPHA BLOCKER
···
## 🟡 P1: Important (Alpha Blockers)
+
### at-identifier Handle Resolution in Endpoints
+
**Added:** 2025-10-18 | **Effort:** 2-3 hours | **Priority:** ALPHA BLOCKER
+
+
**Problem:**
+
Current implementation rejects handles in endpoints that declare `"format": "at-identifier"` in their lexicon schemas, violating atProto best practices and breaking legitimate client usage.
+
+
**Impact:**
+
- ❌ Post creation fails when client sends community handle (e.g., `!gardening.communities.coves.social`)
+
- ❌ Subscribe/unsubscribe endpoints reject handles despite lexicon declaring `at-identifier`
+
- ❌ Block endpoints use `"format": "did"` but should use `at-identifier` for consistency
+
- 🔴 **P0 Issue:** API contract violation - clients following the schema are rejected
+
+
**Root Cause:**
+
Handlers and services validate `strings.HasPrefix(req.Community, "did:")` instead of calling `ResolveCommunityIdentifier()`.
+
+
**Affected Endpoints:**
+
1. **Post Creation** - [create.go:54](../internal/api/handlers/post/create.go#L54), [service.go:51](../internal/core/posts/service.go#L51)
+
- Lexicon declares `at-identifier`: [post/create.json:16](../internal/atproto/lexicon/social/coves/post/create.json#L16)
+
+
2. **Subscribe** - [subscribe.go:52](../internal/api/handlers/community/subscribe.go#L52)
+
- Lexicon declares `at-identifier`: [subscribe.json:16](../internal/atproto/lexicon/social/coves/community/subscribe.json#L16)
+
+
3. **Unsubscribe** - [subscribe.go:120](../internal/api/handlers/community/subscribe.go#L120)
+
- Lexicon declares `at-identifier`: [unsubscribe.json:16](../internal/atproto/lexicon/social/coves/community/unsubscribe.json#L16)
+
+
4. **Block/Unblock** - [block.go:58](../internal/api/handlers/community/block.go#L58), [block.go:132](../internal/api/handlers/community/block.go#L132)
+
- Lexicon declares `"format": "did"`: [block.json:15](../internal/atproto/lexicon/social/coves/community/block.json#L15)
+
- Should be changed to `at-identifier` for consistency and best practice
+
+
**atProto Best Practice (from docs):**
+
- ✅ API endpoints should accept both DIDs and handles via `at-identifier` format
+
- ✅ Resolve handles to DIDs immediately at API boundary
+
- ✅ Use DIDs internally for all business logic and storage
+
- ✅ Handles are weak refs (changeable), DIDs are strong refs (permanent)
+
- ⚠️ Bidirectional verification required (already handled by `identity.CachingResolver`)
+
+
**Solution:**
+
Replace direct DID validation with handle resolution using existing `ResolveCommunityIdentifier()`:
+
+
```go
+
// BEFORE (wrong) ❌
+
if !strings.HasPrefix(req.Community, "did:") {
+
return error
+
}
+
+
// AFTER (correct) ✅
+
communityDID, err := h.communityService.ResolveCommunityIdentifier(ctx, req.Community)
+
if err != nil {
+
if communities.IsNotFound(err) {
+
writeError(w, http.StatusNotFound, "CommunityNotFound", "Community not found")
+
return
+
}
+
writeError(w, http.StatusBadRequest, "InvalidRequest", err.Error())
+
return
+
}
+
// Now use communityDID (guaranteed to be a DID)
+
```
+
+
**Implementation Plan:**
+
1. ✅ **Phase 1 (Alpha Blocker):** Fix post creation endpoint
+
- Update handler validation in `internal/api/handlers/post/create.go`
+
- Update service validation in `internal/core/posts/service.go`
+
- Add integration tests for handle resolution in post creation
+
+
2. 📋 **Phase 2 (Beta):** Fix subscription endpoints
+
- Update subscribe/unsubscribe handlers
+
- Add tests for handle resolution in subscriptions
+
+
3. 📋 **Phase 3 (Beta):** Fix block endpoints
+
- Update lexicon from `"format": "did"` → `"format": "at-identifier"`
+
- Update block/unblock handlers
+
- Add tests for handle resolution in blocking
+
+
**Files to Modify (Phase 1 - Post Creation):**
+
- `internal/api/handlers/post/create.go` - Remove DID validation, add handle resolution
+
- `internal/core/posts/service.go` - Remove DID validation, add handle resolution
+
- `internal/core/posts/interfaces.go` - Add `CommunityService` dependency
+
- `cmd/server/main.go` - Pass community service to post service constructor
+
- `tests/integration/post_creation_test.go` - Add handle resolution test cases
+
+
**Existing Infrastructure:**
+
✅ `ResolveCommunityIdentifier()` already implemented at [service.go:843](../internal/core/communities/service.go#L843)
+
✅ `identity.CachingResolver` handles bidirectional verification and caching
+
✅ Supports both handle (`!name.communities.instance.com`) and DID formats
+
+
**Current Status:**
+
- ⚠️ **BLOCKING POST CREATION PR**: Identified as P0 issue in code review
+
- 📋 Phase 1 (post creation) - To be implemented immediately
+
- 📋 Phase 2-3 (other endpoints) - Deferred to Beta
+
+
---
+
### did:web Domain Verification & hostedByDID Auto-Population
**Added:** 2025-10-11 | **Updated:** 2025-10-16 | **Effort:** 2-3 days | **Priority:** ALPHA BLOCKER
+60 -2
docs/PRD_COMMUNITIES.md
···
## 📍 Beta Features (High Priority - Post Alpha)
### Posts in Communities
**Status:** Lexicon designed, implementation TODO
**Priority:** HIGHEST for Beta 1
-
- [ ] `social.coves.post` already has `community` field ✅
-
- [ ] Create post endpoint (decide: membership validation?)
- [ ] Feed generation for community posts
- [ ] Post consumer (index community posts from firehose)
- [ ] Community post count tracking
···
- `banner` - Blob reference for banner image
- `moderationType` - `"moderator"` or `"sortition"`
- `contentWarnings` - Array of content warning types
- `memberCount` - Cached count
- `subscriberCount` - Cached count
···
---
## Technical Decisions Log
### 2025-10-11: Single Handle Field (atProto-Compliant)
**Decision:** Use single `handle` field containing DNS-resolvable atProto handle; remove `atprotoHandle` field
···
## 📍 Beta Features (High Priority - Post Alpha)
+
### Content Rules System
+
**Status:** Lexicon complete (2025-10-18), implementation TODO
+
**Priority:** CRITICAL for Alpha - Enables community content policies
+
+
Communities can define content posting restrictions via the `contentRules` object in their profile:
+
+
**Key Features:**
+
- ✅ Lexicon: `contentRules` object defined in `social.coves.community.profile`
+
- [ ] Validation: Post creation validates against community rules
+
- [ ] AppView indexing: Index post characteristics (embed_type, text_length)
+
- [ ] Error handling: Clear `ContentRuleViolation` errors
+
+
**Example Use Cases:**
+
- **Text-only communities:** "AskCoves" requires text, blocks all embeds
+
- **Image communities:** "CovesPics" requires at least 1 image
+
- **No restrictions:** General communities allow all content types
+
+
**See:** [PRD_GOVERNANCE.md - Content Rules System](PRD_GOVERNANCE.md#content-rules-system) for full details
+
+
---
+
### Posts in Communities
**Status:** Lexicon designed, implementation TODO
**Priority:** HIGHEST for Beta 1
+
- [x] `social.coves.post` already has `community` field ✅
+
- [x] Removed `postType` enum in favor of content rules ✅ (2025-10-18)
+
- [ ] Create post endpoint with content rules validation
- [ ] Feed generation for community posts
- [ ] Post consumer (index community posts from firehose)
- [ ] Community post count tracking
···
- `banner` - Blob reference for banner image
- `moderationType` - `"moderator"` or `"sortition"`
- `contentWarnings` - Array of content warning types
+
- `contentRules` - Content posting restrictions (see [PRD_GOVERNANCE.md](PRD_GOVERNANCE.md#content-rules-system))
+
- `allowedEmbedTypes` - Array of allowed embed types (images, video, external, record)
+
- `requireText` - Whether text content is required
+
- `minTextLength` / `maxTextLength` - Text length constraints
+
- `requireTitle` - Whether title is required
+
- `minImages` / `maxImages` - Image count constraints
+
- `allowFederated` - Whether federated posts allowed
- `memberCount` - Cached count
- `subscriberCount` - Cached count
···
---
## Technical Decisions Log
+
+
### 2025-10-18: Content Rules Over Post Type Enum
+
**Decision:** Remove `postType` enum from post creation; use flexible `contentRules` in community profile instead
+
+
**Rationale:**
+
- `postType` enum forced users to explicitly select type (bad UX - app should infer from structure)
+
- Enum was rigid - couldn't support nuanced rules like "text required, images optional"
+
- Content rules are more extensible (add new constraints without changing post lexicon)
+
- Follows atProto philosophy: describe data structure, not UI intent
+
- Enables both community restrictions ("text only") AND user filtering ("show videos only")
+
+
**Implementation:**
+
- Community profile contains optional `contentRules` object
+
- Post validation checks structure against rules at creation time
+
- AppView indexes post characteristics (embed_type, text_length) for filtering
+
- Errors use `ContentRuleViolation` instead of `InvalidPostType`
+
+
**Examples:**
+
- "AskCoves": `allowedEmbedTypes: []` + `requireText: true` + `minTextLength: 50`
+
- "CovesPics": `allowedEmbedTypes: ["images"]` + `minImages: 1`
+
- General communities: `contentRules: null` (no restrictions)
+
+
**Trade-offs Accepted:**
+
- Validation logic more complex than simple enum check (but more powerful)
+
- Communities can't programmatically restrict to exact "article" vs "text" types (but structure-based rules are better)
+
+
**See:** [PRD_GOVERNANCE.md - Content Rules System](PRD_GOVERNANCE.md#content-rules-system)
+
+
---
### 2025-10-11: Single Handle Field (atProto-Compliant)
**Decision:** Use single `handle` field containing DNS-resolvable atProto handle; remove `atprotoHandle` field
+134 -72
docs/PRD_GOVERNANCE.md
···
**Current State (2025-10-15):**
- Communities own their own atProto repositories (V2 architecture)
- Instance holds PDS credentials for infrastructure management
-
- Basic authorization exists: only `createdBy` user can update communities
- No moderator management system exists yet
**Note:** Moderator management and advanced governance are **post-alpha** (Beta Phase 1) work. Alpha focuses on basic community CRUD operations.
···
2. **Community lifecycle:** No way to transfer ownership or add co-managers
3. **Scaling moderation:** Single-owner model doesn't scale to large communities
4. **User expectations:** Forum users expect moderator teams, not single-admin models
-
-
**User Stories:**
-
- As a **self-hosted instance owner**, I want to create communities and assign moderators so I don't have to manage everything myself
-
- As a **community creator**, I want to add trusted moderators to help manage the community
-
- As a **moderator**, I want clear permissions on what I can/cannot do
-
- As an **instance admin**, I need emergency moderation powers for compliance/safety
## Architecture Evolution
···
- Should NOT be used for day-to-day community management
- Authority derived from instance DID matching `hostedBy`
-
**Database Schema:**
-
```
-
community_moderators
-
- id (UUID, primary key)
-
- community_did (references communities.did)
-
- moderator_did (user DID)
-
- role (enum: 'creator', 'moderator')
-
- added_by (DID of user who granted role)
-
- added_at (timestamp)
-
- UNIQUE(community_did, moderator_did)
-
```
-
-
**Authorization Checks:**
-
- **Update community profile:** Creator OR Moderator
-
- **Add/remove moderators:** Creator only
-
- **Delete community:** Creator only
-
- **Transfer creator role:** Creator only
-
- **Instance moderation:** Instance admin only (emergency use)
-
-
**Implementation Approach:**
-
- Add `community_moderators` table to schema
-
- Create authorization middleware for XRPC endpoints
-
- Update service layer to check permissions before operations
-
- Store moderator list in both AppView DB and optionally in atProto repository
-
-
**Benefits:**
-
- ✅ Familiar to forum users (creator/moderator model is standard)
-
- ✅ Works for both centralized and self-hosted instances
-
- ✅ Clear separation of concerns (community vs instance authority)
-
- ✅ Easy to implement on top of existing V2 architecture
-
- ✅ Provides foundation for future governance features
-
-
**Limitations:**
-
- ❌ Still centralized (creator has ultimate authority)
-
- ❌ No democratic decision-making
-
- ❌ Moderator removal is unilateral (creator decision)
-
- ❌ No community input on governance changes
-
-
---
### V2: Moderator Tiers & Permissions
···
---
## Implementation Roadmap
### Phase 1: V1 Role-Based System (Months 0-3)
**Goals:**
···
**Success Criteria:**
- Community creators can add/remove moderators
-
- Moderators can update community profile but not delete
- Authorization prevents unauthorized operations
- Works seamlessly for both centralized and self-hosted instances
···
---
## Technical Decisions Log
-
-
### 2025-10-18: Moderator Lexicon Extensibility
-
**Decision:** Use `knownValues` instead of `enum` for moderator roles and permissions in `social.coves.community.moderator` record schema
-
-
**Rationale:**
-
- Moderator records are immutable once published (atProto record semantics)
-
- Closed `enum` values cannot be extended without breaking schema evolution rules
-
- Using `knownValues` allows adding new roles/permissions in Beta Phase 2 without requiring V2 schema migration
-
- Zero cost to fix during alpha planning; expensive to migrate once records exist in production
-
-
**Changes Made:**
-
- `role` field: Changed from `enum: ["moderator", "admin"]` to `knownValues: ["moderator", "admin"]` with `maxLength: 64`
-
- `permissions` array items: Changed from closed enum to `knownValues` with `maxLength: 64`
-
-
**Future Extensibility Examples:**
-
- **New roles**: "owner" (full transfer rights), "trainee" (limited trial moderator), "emeritus" (honorary former moderator)
-
- **New permissions**: "manage_bots", "manage_flairs", "manage_automoderator", "manage_federation", "pin_posts"
-
- Can add these values during Phase 2 (Moderator Tiers & Permissions) without breaking existing moderator records
-
-
**atProto Style Guide Reference:**
-
Per [atproto#4245](https://github.com/bluesky-social/atproto/discussions/4245): "Enum sets are 'closed' and can not be updated or extended without breaking schema evolution rules. For this reason they should almost always be avoided. For strings, `knownValues` provides more flexible alternative."
-
-
**Implementation Status:** ✅ Fixed in lexicon before alpha launch
-
-
---
### 2025-10-11: Moderator Records Storage Location
**Decision:** Store moderator records in community's repository (`at://community_did/social.coves.community.moderator/{tid}`), not user's repository
···
**Current State (2025-10-15):**
- Communities own their own atProto repositories (V2 architecture)
- Instance holds PDS credentials for infrastructure management
+
- Basic authorization exists: only `createdBy` user can update communities (Admins too?)
- No moderator management system exists yet
**Note:** Moderator management and advanced governance are **post-alpha** (Beta Phase 1) work. Alpha focuses on basic community CRUD operations.
···
2. **Community lifecycle:** No way to transfer ownership or add co-managers
3. **Scaling moderation:** Single-owner model doesn't scale to large communities
4. **User expectations:** Forum users expect moderator teams, not single-admin models
## Architecture Evolution
···
- Should NOT be used for day-to-day community management
- Authority derived from instance DID matching `hostedBy`
### V2: Moderator Tiers & Permissions
···
---
+
## Content Rules System
+
+
**Status:** Designed (2025-10-18)
+
**Priority:** Alpha - Enables community-specific content policies
+
+
### Overview
+
+
Content rules allow communities to define restrictions on what types of content can be posted, replacing the rejected `postType` enum approach with flexible, structure-based validation.
+
+
### Lexicon Design
+
+
Content rules are stored in `social.coves.community.profile` under the `contentRules` object:
+
+
```json
+
{
+
"contentRules": {
+
"allowedEmbedTypes": ["images"], // Only images allowed
+
"requireText": true, // Posts must have text
+
"minTextLength": 50, // Minimum 50 characters
+
"maxTextLength": 5000, // Maximum 5000 characters
+
"requireTitle": false, // Title optional
+
"minImages": 1, // At least 1 image
+
"maxImages": 10, // Maximum 10 images
+
"allowFederated": false // No federated posts
+
}
+
}
+
```
+
+
### Example Community Configurations
+
+
**"AskCoves" - Text-Only Q&A:**
+
```json
+
{
+
"contentRules": {
+
"allowedEmbedTypes": [], // No embeds at all
+
"requireText": true,
+
"minTextLength": 50, // Substantive questions only
+
"requireTitle": true, // Must have question title
+
"allowFederated": false
+
}
+
}
+
```
+
+
**"CovesPics" - Image Community:**
+
```json
+
{
+
"contentRules": {
+
"allowedEmbedTypes": ["images"],
+
"requireText": false, // Description optional
+
"minImages": 1, // Must have at least 1 image
+
"maxImages": 20,
+
"allowFederated": true // Allow Bluesky image posts
+
}
+
}
+
```
+
+
**"CovesGeneral" - No Restrictions:**
+
```json
+
{
+
"contentRules": null // Or omit entirely - all content types allowed
+
}
+
```
+
+
### Implementation Flow
+
+
1. **Community Creation/Update:**
+
- Creator/moderator sets `contentRules` in community profile
+
- Rules stored in community's repository (`at://community_did/social.coves.community.profile/self`)
+
- AppView indexes rules for validation
+
+
2. **Post Creation:**
+
- Handler receives post creation request
+
- Fetches community profile (including `contentRules`)
+
- Validates post structure against rules
+
- Returns `ContentRuleViolation` error if validation fails
+
+
3. **Validation Logic:**
+
- Check embed types against `allowedEmbedTypes`
+
- Verify text requirements (`requireText`, `minTextLength`, `maxTextLength`)
+
- Check title requirements (`requireTitle`)
+
- Validate image counts (`minImages`, `maxImages`)
+
- Block federated posts if `allowFederated: false`
+
+
4. **User Filtering (Client-Side):**
+
- AppView indexes derived post characteristics (has_embed, embed_type, text_length)
+
- UI can filter "show only videos" or "show only text posts"
+
- Filters don't need protocol support - just AppView queries
+
+
### Benefits Over `postType` Enum
+
+
✅ **More Flexible:** Communities can define granular rules (e.g., "text required but images optional")
+
✅ **Extensible:** Add new rules without changing post lexicon
+
✅ **Federation-Friendly:** Rules describe structure, not arbitrary types
+
✅ **Client Freedom:** Different clients interpret same data differently
+
✅ **Separation of Concerns:** Post structure (protocol) vs. community policy (governance)
+
+
### Security Considerations
+
+
- **Validation is advisory:** Malicious PDS could ignore rules, but AppView can filter out violating posts
+
- **Rate limiting:** Prevent spam of posts that get rejected for rule violations
+
- **Audit logging:** Track rule violations for moderation review
+
+
---
+
## Implementation Roadmap
+
### Phase 0: Content Rules System (Month 0 - Alpha Blocker)
+
+
**Status:** Lexicon complete, implementation TODO
+
**Priority:** CRITICAL - Required for alpha launch
+
+
**Goals:**
+
- Enable communities to restrict content types
+
- Validate posts against community rules
+
- Support common use cases (text-only, images-only, etc.)
+
+
**Deliverables:**
+
- [x] Lexicon: `contentRules` object in `social.coves.community.profile` ✅
+
- [ ] Go structs: `ContentRules` type in community models
+
- [ ] Repository: Parse and store `contentRules` from community profiles
+
- [ ] Service: `ValidatePostAgainstRules(post, community)` function
+
- [ ] Handler: Integrate validation into `social.coves.post.create`
+
- [ ] AppView indexing: Index post characteristics (embed_type, text_length, etc.)
+
- [ ] Tests: Comprehensive rule validation tests
+
- [ ] Documentation: Content rules guide for community creators
+
+
**Success Criteria:**
+
- "AskCoves" text-only community rejects image posts
+
- "CovesPics" image community requires at least one image
+
- Validation errors are clear and actionable
+
- No performance impact on post creation (< 10ms validation)
+
+
---
+
### Phase 1: V1 Role-Based System (Months 0-3)
**Goals:**
···
**Success Criteria:**
- Community creators can add/remove moderators
+
- Moderators can update community profile (including content rules) but not delete
- Authorization prevents unauthorized operations
- Works seamlessly for both centralized and self-hosted instances
···
---
## Technical Decisions Log
### 2025-10-11: Moderator Records Storage Location
**Decision:** Store moderator records in community's repository (`at://community_did/social.coves.community.moderator/{tid}`), not user's repository
+840
docs/PRD_POSTS.md
···
···
+
# Posts PRD: Forum Content System
+
+
**Status:** ✅ Alpha CREATE Complete (2025-10-19) | Get/Update/Delete/Voting TODO
+
**Owner:** Platform Team
+
**Last Updated:** 2025-10-19
+
+
## 🎯 Implementation Status
+
+
### ✅ COMPLETED (Alpha - 2025-10-19)
+
- **Post Creation:** Full write-forward to community PDS with real-time Jetstream indexing
+
- **Handler Layer:** HTTP endpoint with authentication, validation, and security checks
+
- **Service Layer:** Business logic with token refresh and community resolution
+
- **Repository Layer:** PostgreSQL storage with proper indexing
+
- **Jetstream Consumer:** Real-time indexing with security validation
+
- **Database Migration:** Posts table created (migration 011)
+
- **E2E Tests:** Live PDS + Jetstream integration tests passing
+
- **at-identifier Support:** All 4 formats (DIDs, canonical, @-prefixed, scoped handles)
+
+
### ⚠️ DEFERRED TO BETA
+
- Content rules validation (text-only, image-only communities)
+
- Post read operations (get, list)
+
- Post update/edit operations
+
- Post deletion
+
- Voting system (upvotes/downvotes)
+
- Derived characteristics indexing (embed_type, text_length, etc.)
+
+
**See:** [IMPLEMENTATION_POST_CREATION.md](IMPLEMENTATION_POST_CREATION.md) for complete implementation details.
+
+
---
+
+
## Overview
+
+
Posts are the core content unit in Coves communities. Built on atProto, each post is stored in the **community's repository** and indexed by the AppView for discovery and interaction. Posts support rich text, embeds, voting, tagging, and federation with other atProto platforms.
+
+
## Architecture
+
+
### atProto Data Flow
+
Posts follow the community-owned repository pattern, matching the V2 Communities architecture:
+
+
```
+
User creates post → Written to COMMUNITY's PDS repository (using community credentials) →
+
Firehose broadcasts event → AppView Jetstream consumer indexes →
+
Post appears in feeds
+
```
+
+
**Repository Structure:**
+
```
+
Repository: at://did:plc:community789/social.coves.post.record/3k2a4b5c6d7e
+
Owner: did:plc:community789 (community owns the post)
+
Author: did:plc:user123 (tracked in record metadata)
+
Hosted By: did:web:coves.social (instance manages community credentials)
+
```
+
+
**Key Architectural Principles:**
+
- ✅ Communities own posts (posts live in community repos, like traditional forums)
+
- ✅ Author tracked in metadata (post.author field references user DID)
+
- ✅ Communities are portable (migrate instance = posts move with community)
+
- ✅ Matches V2 Communities pattern (community owns repository, instance manages credentials)
+
- ✅ Write operations use community's PDS credentials (not user credentials)
+
+
---
+
+
## Alpha Features (MVP - Ship First)
+
+
### Content Rules Integration
+
**Status:** Lexicon complete (2025-10-18), validation TODO
+
**Priority:** CRITICAL - Required for community content policies
+
+
Posts are validated against community-specific content rules at creation time. Communities can restrict:
+
- Allowed embed types (images, video, external, record)
+
- Text requirements (min/max length, required/optional)
+
- Title requirements
+
- Image count limits
+
- Federated content policies
+
+
**See:** [PRD_GOVERNANCE.md - Content Rules System](PRD_GOVERNANCE.md#content-rules-system) for full details.
+
+
**Implementation checklist:**
+
- [x] Lexicon: `contentRules` in `social.coves.community.profile` ✅
+
- [x] Lexicon: `postType` removed from `social.coves.post.create` ✅
+
- [ ] Validation: `ValidatePostAgainstRules()` service function
+
- [ ] Handler: Integrate validation in post creation endpoint
+
- [ ] AppView: Index derived characteristics (embed_type, text_length, etc.)
+
- [ ] Tests: Validate content rule enforcement
+
+
---
+
+
### Core Post Management
+
**Status:** ✅ CREATE COMPLETE (2025-10-19) - Get/Update/Delete TODO
+
**Priority:** CRITICAL - Posts are the foundation of the platform
+
+
#### Create Post
+
- [x] Lexicon: `social.coves.post.record` ✅
+
- [x] Lexicon: `social.coves.post.create` ✅
+
- [x] Removed `postType` enum in favor of content rules ✅ (2025-10-18)
+
- [x] Removed `postType` from record and get lexicons ✅ (2025-10-18)
+
- [x] **Handler:** `POST /xrpc/social.coves.post.create` ✅ (Alpha - see IMPLEMENTATION_POST_CREATION.md)
+
- ✅ Accept: community (DID/handle), title (optional), content, facets, embed, contentLabels
+
- ✅ Validate: User is authenticated, community exists, content within limits
+
- ✅ Write: Create record in **community's PDS repository**
+
- ✅ Return: AT-URI and CID of created post
+
- ⚠️ Content rules validation deferred to Beta
+
- [x] **Service Layer:** `PostService.Create()` ✅
+
- ✅ Resolve community identifier to DID (supports all 4 at-identifier formats)
+
- ✅ Validate community exists and is not private
+
- ✅ Fetch community from AppView
+
- ⚠️ **Validate post against content rules** DEFERRED (see [PRD_GOVERNANCE.md](PRD_GOVERNANCE.md#content-rules-system))
+
- ✅ Fetch community's PDS credentials with automatic token refresh
+
- ✅ Build post record with author DID, timestamp, content
+
- ✅ **Write to community's PDS** using community's access token
+
- ✅ Return URI/CID for AppView indexing
+
- [x] **Validation:** ✅
+
- ✅ Community reference is valid (supports DIDs and handles)
+
- ✅ Content length ≤ 50,000 characters
+
- ✅ Title (if provided) ≤ 3,000 bytes
+
- ✅ ContentLabels are from known values (nsfw, spoiler, violence)
+
- ⚠️ **Content rules compliance:** DEFERRED TO BETA
+
- Check embed types against `allowedEmbedTypes`
+
- Verify `requireText` / `minTextLength` / `maxTextLength`
+
- Verify `requireTitle` if set
+
- Check image counts against `minImages` / `maxImages`
+
- Block federated posts if `allowFederated: false`
+
- Return `ContentRuleViolation` error if validation fails
+
- [x] **E2E Test:** Create text post → Write to **community's PDS** → Index via Jetstream → Verify in AppView ✅
+
+
#### Get Post
+
- [x] Lexicon: `social.coves.post.get` ✅
+
- [ ] **Handler:** `GET /xrpc/social.coves.post.get?uri=at://...`
+
- Accept: AT-URI of post
+
- Return: Full post view with author, community, stats, viewer state
+
- [ ] **Service Layer:** `PostService.Get(uri, viewerDID)`
+
- Fetch post from AppView PostgreSQL
+
- Join with user/community data
+
- Calculate stats (upvotes, downvotes, score, comment count)
+
- Include viewer state (vote status, saved status, tags)
+
- [ ] **Repository:** `PostRepository.GetByURI()`
+
- Single query with JOINs for author, community, stats
+
- Handle missing posts gracefully (deleted or not indexed)
+
- [ ] **E2E Test:** Get post by URI → Verify all fields populated
+
+
#### Update Post
+
- [x] Lexicon: `social.coves.post.update` ✅
+
- [ ] **Handler:** `POST /xrpc/social.coves.post.update`
+
- Accept: uri, title, content, facets, embed, contentLabels, editNote
+
- Validate: User is post author, within 24-hour edit window
+
- Write: Update record in **community's PDS**
+
- Return: New CID
+
- [ ] **Service Layer:** `PostService.Update()`
+
- Fetch existing post from AppView
+
- Verify authorship (post.author == authenticated user DID)
+
- Verify edit window (createdAt + 24 hours > now)
+
- Fetch community's PDS credentials (with token refresh)
+
- **Update record in community's PDS** using community's access token
+
- Track edit timestamp (editedAt field)
+
- [ ] **Edit Window:** 24 hours from creation (hardcoded for Alpha)
+
- [ ] **Edit Note:** Optional explanation field (stored in record)
+
- [ ] **E2E Test:** Update post → Verify edit reflected in AppView
+
+
#### Delete Post
+
- [x] Lexicon: `social.coves.post.delete` ✅
+
- [ ] **Handler:** `POST /xrpc/social.coves.post.delete`
+
- Accept: uri
+
- Validate: User is post author OR community moderator
+
- Write: Delete record from **community's PDS**
+
- [ ] **Service Layer:** `PostService.Delete()`
+
- Verify authorship OR moderator permission
+
- Fetch community's PDS credentials
+
- **Delete from community's PDS** (broadcasts DELETE event to firehose)
+
- Consumer handles soft delete in AppView
+
- [ ] **AppView Behavior:** Mark as deleted (soft delete), hide from feeds
+
- [ ] **Moderator Delete:** Community moderators can delete any post in their community
+
- [ ] **E2E Test:** Delete post → Verify hidden from queries
+
+
---
+
+
### Post Content Features
+
+
#### Rich Text Support
+
- [x] Lexicon: Facets reference `social.coves.richtext.facet` ✅
+
- [ ] **Supported Facets:**
+
- Mentions: `@user.bsky.social` → Links to user profile
+
- Links: `https://example.com` → Clickable URLs
+
- Community mentions: `!community@instance` → Links to community
+
- Hashtags: `#topic` → Tag-based discovery (Future)
+
- [ ] **Implementation:**
+
- Store facets as JSON array in post record
+
- Validate byte ranges match content
+
- Render facets in AppView responses
+
+
#### Embeds (Alpha Scope)
+
- [x] Lexicon: Embed union type ✅
+
- [ ] **Alpha Support:**
+
- **Images:** Upload to community's PDS blob storage, reference in embed
+
- **External Links:** URL, title, description, thumbnail (client-fetched)
+
- **Quoted Posts:** Reference another post's AT-URI
+
- [ ] **Defer to Beta:**
+
- Video embeds (requires video processing infrastructure)
+
+
#### Content Labels
+
- [x] Lexicon: Self-applied labels ✅
+
- [ ] **Alpha Labels:**
+
- `nsfw` - Not safe for work
+
- `spoiler` - Spoiler content (blur/hide by default)
+
- `violence` - Violent or graphic content
+
- [ ] **Implementation:**
+
- Store as string array in post record
+
- AppView respects labels in feed filtering
+
- Client renders appropriate warnings/blurs
+
+
---
+
+
### Voting System
+
+
#### Upvotes & Downvotes
+
- [x] Lexicon: `social.coves.interaction.vote` ✅
+
- [ ] **Handler:** `POST /xrpc/social.coves.interaction.createVote`
+
- Accept: subject (post AT-URI), direction (up/down)
+
- Write: Create vote record in **user's repository**
+
- [ ] **Handler:** `POST /xrpc/social.coves.interaction.deleteVote`
+
- Accept: voteUri (AT-URI of vote record)
+
- Write: Delete vote record from **user's repository**
+
- [ ] **Vote Toggling:**
+
- Upvote → Upvote = Delete upvote
+
- Upvote → Downvote = Delete upvote + Create downvote
+
- No vote → Upvote = Create upvote
+
- [ ] **Downvote Controls (Alpha):**
+
- Global default: Downvotes enabled
+
- Community-level toggle: `allowDownvotes` (Boolean in community.profile)
+
- Instance-level toggle: Environment variable `ALLOW_DOWNVOTES` (Future)
+
- [ ] **AppView Indexing:**
+
- Consumer tracks vote CREATE/DELETE events
+
- Aggregate counts: upvotes, downvotes, score (upvotes - downvotes)
+
- Track viewer's vote state (for "already voted" UI)
+
- [ ] **E2E Test:** Create vote → Index → Verify count updates → Delete vote → Verify count decrements
+
+
**Note:** Votes live in user's repository (user owns their voting history), but posts live in community's repository.
+
+
#### Vote Statistics
+
- [x] Lexicon: `postStats` in post view ✅
+
- [ ] **Stats Fields:**
+
- `upvotes` - Total upvote count
+
- `downvotes` - Total downvote count (0 if community disables)
+
- `score` - Calculated score (upvotes - downvotes)
+
- `commentCount` - Total comments (placeholder for Beta)
+
- `shareCount` - Share tracking (Future)
+
- `tagCounts` - Aggregate tag counts (Future)
+
+
---
+
+
### Jetstream Consumer (Indexing)
+
+
#### Post Event Handling
+
- [x] **Consumer:** `PostConsumer.HandlePostEvent()` ✅ (2025-10-19)
+
- ✅ Listen for `social.coves.post.record` CREATE from **community repositories**
+
- ✅ Parse post record, extract author DID and community DID (from AT-URI owner)
+
- ⚠️ **Derive post characteristics:** DEFERRED (embed_type, text_length, has_title, has_embed for content rules filtering)
+
- ✅ Insert in AppView PostgreSQL (CREATE only - UPDATE/DELETE deferred)
+
- ✅ Index: uri, cid, author_did, community_did, title, content, created_at, indexed_at
+
- ✅ **Security Validation:**
+
- ✅ Verify event.repo matches community DID (posts must come from community repos)
+
- ✅ Verify community exists in AppView (foreign key integrity)
+
- ✅ Verify author exists in AppView (foreign key integrity)
+
- ✅ Idempotent indexing for Jetstream replays
+
+
#### Vote Event Handling
+
- [ ] **Consumer:** `PostConsumer.HandleVoteEvent()` - DEFERRED TO BETA (voting system not yet implemented)
+
- Listen for `social.coves.interaction.vote` CREATE/DELETE from **user repositories**
+
- Parse subject URI (extract post)
+
- Increment/decrement vote counts atomically
+
- Track vote URI for viewer state queries
+
- **Validation:** Verify event.repo matches voter DID (votes must come from user repos)
+
+
#### Error Handling
+
- [x] Invalid community references → Reject post (foreign key enforcement) ✅
+
- [x] Invalid author references → Reject post (foreign key enforcement) ✅
+
- [x] Malformed records → Skip indexing, log error ✅
+
- [x] Duplicate events → Idempotent operations (unique constraint on URI) ✅
+
- [x] Posts from user repos → Reject (repository DID validation) ✅
+
+
---
+
+
### Database Schema
+
+
#### Posts Table ✅ IMPLEMENTED (2025-10-19)
+
+
**Migration:** [internal/db/migrations/011_create_posts_table.sql](../internal/db/migrations/011_create_posts_table.sql)
+
+
```sql
+
CREATE TABLE posts (
+
id BIGSERIAL PRIMARY KEY,
+
uri TEXT UNIQUE NOT NULL, -- AT-URI (at://community_did/collection/rkey)
+
cid TEXT NOT NULL, -- Content ID
+
rkey TEXT NOT NULL, -- Record key (TID)
+
author_did TEXT NOT NULL, -- Author's DID (from record metadata)
+
community_did TEXT NOT NULL, -- Community DID (from AT-URI owner)
+
title TEXT, -- Post title (nullable)
+
content TEXT, -- Post content
+
content_facets JSONB, -- Rich text facets
+
embed JSONB, -- Embedded content
+
content_labels TEXT[], -- Self-applied labels
+
+
-- ⚠️ Derived characteristics DEFERRED TO BETA (for content rules filtering)
+
-- Will be added when content rules are implemented:
+
-- embed_type TEXT, -- images, video, external, record (NULL if no embed)
+
-- text_length INT NOT NULL DEFAULT 0, -- Character count of content
+
-- has_title BOOLEAN NOT NULL DEFAULT FALSE,
+
-- has_embed BOOLEAN NOT NULL DEFAULT FALSE,
+
+
created_at TIMESTAMPTZ NOT NULL, -- Author's timestamp
+
edited_at TIMESTAMPTZ, -- Last edit timestamp
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When indexed
+
deleted_at TIMESTAMPTZ, -- Soft delete
+
+
-- Stats (denormalized for performance)
+
upvote_count INT NOT NULL DEFAULT 0,
+
downvote_count INT NOT NULL DEFAULT 0,
+
score INT NOT NULL DEFAULT 0, -- upvote_count - downvote_count
+
comment_count INT NOT NULL DEFAULT 0,
+
+
CONSTRAINT fk_author FOREIGN KEY (author_did) REFERENCES users(did) ON DELETE CASCADE,
+
CONSTRAINT fk_community FOREIGN KEY (community_did) REFERENCES communities(did) ON DELETE CASCADE
+
);
+
+
-- ✅ Implemented indexes
+
CREATE INDEX idx_posts_community_created ON posts(community_did, created_at DESC) WHERE deleted_at IS NULL;
+
CREATE INDEX idx_posts_community_score ON posts(community_did, score DESC, created_at DESC) WHERE deleted_at IS NULL;
+
CREATE INDEX idx_posts_author ON posts(author_did, created_at DESC);
+
CREATE INDEX idx_posts_uri ON posts(uri);
+
+
-- ⚠️ Deferred until content rules are implemented:
+
-- CREATE INDEX idx_posts_embed_type ON posts(community_did, embed_type) WHERE deleted_at IS NULL;
+
```
+
+
#### Votes Table
+
```sql
+
CREATE TABLE votes (
+
id BIGSERIAL PRIMARY KEY,
+
uri TEXT UNIQUE NOT NULL, -- Vote record AT-URI (at://voter_did/collection/rkey)
+
cid TEXT NOT NULL,
+
rkey TEXT NOT NULL,
+
voter_did TEXT NOT NULL, -- User who voted (from AT-URI owner)
+
subject_uri TEXT NOT NULL, -- Post/comment AT-URI
+
direction TEXT NOT NULL CHECK (direction IN ('up', 'down')),
+
created_at TIMESTAMPTZ NOT NULL,
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
deleted_at TIMESTAMPTZ,
+
+
CONSTRAINT fk_voter FOREIGN KEY (voter_did) REFERENCES users(did),
+
UNIQUE (voter_did, subject_uri, deleted_at) -- One active vote per user per subject
+
);
+
+
CREATE INDEX idx_votes_subject ON votes(subject_uri, direction) WHERE deleted_at IS NULL;
+
CREATE INDEX idx_votes_voter_subject ON votes(voter_did, subject_uri) WHERE deleted_at IS NULL;
+
```
+
+
---
+
+
## Alpha Blockers (Must Complete)
+
+
### Critical Path
+
- [x] **Post CREATE Endpoint:** ✅ COMPLETE (2025-10-19)
+
- ✅ Handler with authentication and validation
+
- ✅ Service layer with business logic
+
- ✅ Repository layer for database access
+
- ⚠️ Get, Update, Delete deferred to Beta
+
- [x] **Community Credentials:** ✅ Use community's PDS credentials for post writes
+
- [x] **Token Refresh Integration:** ✅ Reuse community token refresh logic for post operations
+
- [x] **Jetstream Consumer:** ✅ Posts indexed in real-time (2025-10-19)
+
- ✅ CREATE operations indexed
+
- ✅ Security validation (repository ownership)
+
- ⚠️ UPDATE/DELETE deferred until those features exist
+
- ⚠️ Derived characteristics deferred to Beta
+
- [x] **Database Migrations:** ✅ Posts table created (migration 011)
+
- [x] **E2E Tests:** ✅ Full flow tested (handler → community PDS → Jetstream → AppView)
+
- ✅ Service layer tests (9 subtests)
+
- ✅ Repository tests (2 subtests)
+
- ✅ Handler security tests (10+ subtests)
+
- ✅ Live PDS + Jetstream E2E test
+
- [x] **Community Integration:** ✅ Posts correctly reference communities via at-identifiers
+
- [x] **at-identifier Support:** ✅ All 4 formats supported (DIDs, canonical, @-prefixed, scoped)
+
- [ ] **Content Rules Validation:** ⚠️ DEFERRED TO BETA - Posts validated against community content rules
+
- [ ] **Vote System:** ⚠️ DEFERRED TO BETA - Upvote/downvote with community-level controls
+
- [ ] **Moderator Permissions:** ⚠️ DEFERRED TO BETA - Community moderators can delete posts
+
+
### Testing Requirements
+
- [x] Create text post in community → Appears in AppView ✅
+
- [x] Post lives in community's repository (verify AT-URI owner) ✅
+
- [x] Post written to PDS → Broadcast to Jetstream → Indexed in AppView ✅
+
- [x] Handler security: Rejects client-provided authorDid ✅
+
- [x] Handler security: Requires authentication ✅
+
- [x] Handler security: Validates request body size ✅
+
- [x] Handler security: All 4 at-identifier formats accepted ✅
+
- [x] Consumer security: Rejects posts from wrong repository ✅
+
- [x] Consumer security: Verifies community and author exist ✅
+
- [ ] **Content rules validation:** Text-only community rejects image posts ⚠️ DEFERRED
+
- [ ] **Content rules validation:** Image community rejects posts without images ⚠️ DEFERRED
+
- [ ] **Content rules validation:** Post with too-short text rejected ⚠️ DEFERRED
+
- [ ] **Content rules validation:** Federated post rejected if `allowFederated: false` ⚠️ DEFERRED
+
- [ ] Update post within 24 hours → Edit reflected ⚠️ DEFERRED
+
- [ ] Delete post as author → Hidden from queries ⚠️ DEFERRED
+
- [ ] Delete post as moderator → Hidden from queries ⚠️ DEFERRED
+
- [ ] Upvote post → Count increments ⚠️ DEFERRED
+
- [ ] Downvote post → Count increments (if enabled) ⚠️ DEFERRED
+
- [ ] Toggle vote → Counts update correctly ⚠️ DEFERRED
+
- [ ] Community with downvotes disabled → Downvote returns error ⚠️ DEFERRED
+
+
---
+
+
## Beta Features (Post-Alpha)
+
+
### Advanced Post Types
+
**Status:** Deferred - Simplify for Alpha
+
**Rationale:** Text posts are sufficient for MVP, other types need more infrastructure
+
+
- [ ] **Image Posts:** Full image upload/processing pipeline
+
- Multi-image support (up to 4 images)
+
- Upload to community's PDS blob storage
+
- Thumbnail generation
+
- Alt text requirements (accessibility)
+
+
- [ ] **Video Posts:** Video hosting and processing
+
- Video upload to community's PDS blob storage
+
- Thumbnail extraction
+
- Format validation
+
- Streaming support
+
+
- [ ] **Microblog Posts:** Bluesky federation integration
+
- Fetch Bluesky posts by AT-URI
+
- Display inline with native posts
+
- Track original author info
+
- Federation metadata
+
+
- [ ] **Decision Point:** Remove "Article" type entirely?
+
- Obsoleted by planned RSS aggregation service
+
- LLMs will break down articles into digestible content
+
- May not need native article posting
+
+
### Post Interaction Features
+
+
#### Tagging System
+
- [x] Lexicon: `social.coves.interaction.tag` ✅
+
- [ ] **Known Tags:** helpful, insightful, spam, hostile, offtopic, misleading
+
- [ ] **Community Custom Tags:** Communities define their own tags
+
- [ ] **Aggregate Counts:** Track tag distribution on posts
+
- [ ] **Moderation Integration:** High spam/hostile tags trigger tribunal review
+
- [ ] **Reputation Impact:** Helpful/insightful tags boost author reputation
+
- [ ] **Tag Storage:** Tags live in **user's repository** (users own their tags)
+
+
#### Crossposting
+
- [x] Lexicon: `social.coves.post.crosspost` ✅
+
- [ ] **Crosspost Tracking:** Share post to multiple communities
+
- [ ] **Implementation:** Create new post record in each community's repository
+
- [ ] **Crosspost Chain:** Track all crosspost relationships
+
- [ ] **Deduplication:** Show original + crosspost count (don't spam feeds)
+
- [ ] **Rules:** Communities can disable crossposting
+
+
#### Save Posts
+
- [ ] **Lexicon:** Create `social.coves.actor.savedPost` record type
+
- [ ] **Functionality:** Bookmark posts for later reading
+
- [ ] **Private List:** Saved posts stored in **user's repository**
+
- [ ] **AppView Query:** Endpoint to fetch user's saved posts
+
+
### Post Search
+
- [x] Lexicon: `social.coves.post.search` ✅
+
- [ ] **Search Parameters:**
+
- Query string (q)
+
- Filter by community
+
- Filter by author
+
- Filter by post type
+
- Filter by tags
+
- Sort: relevance, new, top
+
- Timeframe: hour, day, week, month, year, all
+
- [ ] **Implementation:**
+
- PostgreSQL full-text search (tsvector on title + content)
+
- Relevance ranking algorithm
+
- Pagination with cursor
+
+
### Edit History
+
- [ ] **Track Edits:** Store edit history in AppView (not in atProto record)
+
- [ ] **Edit Diff:** Show what changed between versions
+
- [ ] **Edit Log:** List all edits with timestamps and edit notes
+
- [ ] **Revision Viewing:** View previous versions of post
+
+
### Advanced Voting
+
+
#### Vote Weight by Reputation
+
- [ ] **Reputation Multiplier:** High-reputation users' votes count more
+
- [ ] **Community-Specific:** Reputation calculated per-community
+
- [ ] **Transparency:** Show vote weight in moderation logs (not public)
+
+
#### Fuzzing & Vote Obfuscation
+
- [ ] **Count Fuzzing:** Add noise to vote counts (prevent manipulation detection)
+
- [ ] **Delay Display:** Don't show exact counts for new posts (first hour)
+
- [ ] **Rate Limiting:** Prevent vote brigading
+
+
---
+
+
## Future Features
+
+
### Federation
+
+
#### Bluesky Integration
+
- [ ] **Display Bluesky Posts:** Show Bluesky posts in community feeds (microblog type)
+
- [ ] **Original Author Info:** Track Bluesky user metadata
+
- [ ] **No Native Commenting:** Users see Bluesky posts, can't comment (yet)
+
- [ ] **Reference Storage:** Store Bluesky AT-URI, don't duplicate content
+
+
#### ActivityPub Integration
+
- [ ] **Lemmy/Mbin Posts:** Convert ActivityPub posts to Coves posts
+
- [ ] **Bidirectional Sync:** Coves posts appear on Lemmy instances
+
- [ ] **User Identity Mapping:** Assign DIDs to ActivityPub users
+
- [ ] **Action Translation:** Upvotes ↔ ActivityPub likes
+
+
### Advanced Features
+
+
#### Post Scheduling
+
- [ ] Schedule posts for future publishing
+
- [ ] Edit scheduled posts before they go live
+
- [ ] Cancel scheduled posts
+
+
#### Post Templates
+
- [ ] Communities define post templates
+
- [ ] Auto-fill fields for common post types
+
- [ ] Game threads, event announcements, etc.
+
+
#### Polls
+
- [ ] Create polls in posts
+
- [ ] Multiple choice, ranked choice, approval voting
+
- [ ] Time-limited voting windows
+
- [ ] Results visualization
+
+
#### Location-Based Posting
+
- [x] Lexicon: `location` field in post record ✅
+
- [ ] **Geo-Tagging:** Attach coordinates to posts
+
- [ ] **Community Rules:** Require location for certain posts (local events)
+
- [ ] **Privacy:** User controls location precision
+
- [ ] **Discovery:** Filter posts by location
+
+
---
+
+
## Technical Decisions Log
+
+
### 2025-10-18: Content Rules Over Post Type Enum
+
**Decision:** Remove `postType` from post creation input; validate posts against community's `contentRules` instead
+
+
**Rationale:**
+
- `postType` enum forced users to explicitly select type (bad UX - app should infer from structure)
+
- Structure-based validation is more flexible ("text required, images optional" vs rigid type categories)
+
- Content rules are extensible without changing post lexicon
+
- Enables both community restrictions (governance) AND user filtering (UI preferences)
+
- Follows atProto philosophy: describe data structure, not UI intent
+
+
**Implementation:**
+
- Post creation no longer accepts `postType` parameter
+
- Community profile contains optional `contentRules` object
+
- Handler validates post structure against community's content rules
+
- AppView indexes derived characteristics (embed_type, text_length, has_title, has_embed)
+
- Validation error changed from `InvalidPostType` to `ContentRuleViolation`
+
+
**Database Changes:**
+
- Remove `post_type` enum column
+
- Add derived fields: `embed_type`, `text_length`, `has_title`, `has_embed`
+
- Add index on `embed_type` for filtering
+
+
**Example Rules:**
+
- Text-only community: `allowedEmbedTypes: []` + `requireText: true`
+
- Image community: `allowedEmbedTypes: ["images"]` + `minImages: 1`
+
- No restrictions: `contentRules: null`
+
+
**See:** [PRD_GOVERNANCE.md - Content Rules System](PRD_GOVERNANCE.md#content-rules-system)
+
+
---
+
+
### 2025-10-18: Posts Live in Community Repositories
+
**Decision:** Posts are stored in community's repository, not user's repository
+
+
**Rationale:**
+
- **Matches V2 Communities Architecture:** Communities own their repositories
+
- **Traditional Forum Model:** Community owns content, author tracked in metadata
+
- **Simpler Permissions:** Use community credentials for all post writes
+
- **Portability:** Posts migrate with community when changing instances
+
- **Moderation:** Community has full control over content
+
- **Reuses Token Refresh:** Can leverage existing community credential management
+
+
**Implementation Details:**
+
- Post AT-URI: `at://community_did/social.coves.post.record/tid`
+
- Write operations use community's PDS credentials (encrypted, stored in AppView)
+
- Author tracked in post record's `author` field (DID)
+
- Moderators can delete any post in their community
+
- Token refresh reuses community's refresh logic
+
+
**Trade-offs vs User-Owned Posts:**
+
- ❌ Users can't take posts when leaving community/instance
+
- ❌ Less "web3" (content not user-owned)
+
- ✅ Traditional forum UX (users expect community to own content)
+
- ✅ Simpler implementation (one credential store per community)
+
- ✅ Easier moderation (community has full control)
+
- ✅ Posts move with community during migration
+
+
**Comparison to Bluesky:**
+
- Bluesky: Users own posts (posts in user repo)
+
- Coves: Communities own posts (posts in community repo)
+
- This is acceptable - different platforms, different models
+
- Still atProto-compliant (just different ownership pattern)
+
+
---
+
+
### 2025-10-18: Votes Live in User Repositories
+
**Decision:** Vote records are stored in user's repository, not community's
+
+
**Rationale:**
+
- Users own their voting history (personal preference)
+
- Matches Bluesky pattern (likes in user's repo)
+
- Enables portable voting history across instances
+
- User controls their own voting record
+
+
**Implementation Details:**
+
- Vote AT-URI: `at://user_did/social.coves.interaction.vote/tid`
+
- Write operations use user's PDS credentials
+
- Subject field references post AT-URI (in community's repo)
+
- Consumer aggregates votes from all users into post stats
+
+
---
+
+
### 2025-10-18: Simplify Post Types for Alpha
+
**Decision:** Launch with text posts only, defer other embed types to Beta
+
**Status:** SUPERSEDED by content rules approach (see above)
+
+
**Rationale:**
+
- Text posts are sufficient for forum discussions (core use case)
+
- Image/video embeds require additional infrastructure (blob storage, processing)
+
- Article format can be handled with long-form text posts
+
- Microblog type is for Bluesky federation (not immediate priority)
+
- Simplicity accelerates alpha launch
+
+
**Updated Approach (2025-10-18):**
+
- Post structure determines "type" (not explicit enum)
+
- Communities use `contentRules` to restrict embed types
+
- AppView derives `embed_type` from post structure for filtering
+
- More flexible than rigid type system
+
+
---
+
+
### 2025-10-18: Include Downvotes with Community Controls
+
**Decision:** Support both upvotes and downvotes, with toggles to disable downvotes
+
+
**Rationale:**
+
- Downvotes provide valuable signal for content quality
+
- Some communities prefer upvote-only (toxic negativity concerns)
+
- Instance operators should have global control option
+
- Reddit/HN have proven downvotes work with good moderation
+
+
**Implementation:**
+
- Community-level: `allowDownvotes` boolean in community profile
+
- Instance-level: Environment variable `ALLOW_DOWNVOTES` (future)
+
- Downvote attempts on disabled communities return error
+
- Stats show 0 downvotes when disabled
+
+
---
+
+
### 2025-10-18: 24-Hour Edit Window (Hardcoded for Alpha)
+
**Decision:** Posts can be edited for 24 hours after creation
+
+
**Rationale:**
+
- Allows fixing typos and errors
+
- Prevents historical revisionism (can't change old posts)
+
- 24 hours balances flexibility with integrity
+
- Future: Community-configurable edit windows
+
+
**Future Enhancements:**
+
- Edit history tracking (show what changed)
+
- Community-specific edit windows (0-72 hours)
+
- Moderator override (edit any post)
+
+
---
+
+
### 2025-10-18: Comments Separate from Posts PRD
+
**Decision:** Comments get their own dedicated PRD
+
+
**Rationale:**
+
- Comments are complex enough to warrant separate planning
+
- Threaded replies, vote inheritance, moderation all need design
+
- Posts are usable without comments (voting, tagging still work)
+
- Allows shipping posts sooner
+
+
**Scope Boundary:**
+
- **Posts PRD:** Post CRUD, voting, tagging, search
+
- **Comments PRD:** Comment threads, reply depth, sorting, moderation
+
+
---
+
+
### 2025-10-18: Feeds Separate from Posts PRD
+
**Decision:** Feed generation gets its own PRD
+
+
**Rationale:**
+
- Feed algorithms are complex (ranking, personalization, filtering)
+
- Posts need to exist before feeds can be built
+
- Feed work includes: Home feed, Community feed, All feed, read state tracking
+
- Allows iterating on feed algorithms independently
+
+
**Scope Boundary:**
+
- **Posts PRD:** Post creation, indexing, retrieval
+
- **Feeds PRD:** Feed generation, ranking algorithms, read state, personalization
+
+
---
+
+
## Success Metrics
+
+
### Alpha Launch Checklist ✅ COMPLETE (2025-10-19)
+
- [x] Users can create text posts in communities ✅
+
- [x] Posts are stored in community's repository (verify AT-URI) ✅
+
- [x] Posts use community's PDS credentials for writes ✅
+
- [x] Posts are indexed from firehose within 1 second ✅ (real-time Jetstream)
+
- [x] E2E tests cover full write-forward flow ✅
+
- [x] Database handles posts without performance issues ✅
+
- [x] Handler security tests passing (authentication, validation, body size) ✅
+
- [x] Consumer security validation (repository ownership, community/author checks) ✅
+
- [x] All 4 at-identifier formats supported ✅
+
+
### Beta Checklist (TODO)
+
- [ ] Post editing works within 24-hour window ⚠️ DEFERRED
+
- [ ] Upvote/downvote system functional ⚠️ DEFERRED
+
- [ ] Community downvote toggle works ⚠️ DEFERRED
+
- [ ] Post deletion soft-deletes and hides from queries ⚠️ DEFERRED
+
- [ ] Moderators can delete posts in their community ⚠️ DEFERRED
+
- [ ] Get post endpoint returns full post view with stats ⚠️ DEFERRED
+
- [ ] Content rules validation working ⚠️ DEFERRED
+
- [ ] Database handles 100,000+ posts (load testing)
+
+
### Beta Goals
+
- [ ] All post types supported (text, image, video, microblog)
+
- [ ] Tagging system enables community moderation
+
- [ ] Post search returns relevant results
+
- [ ] Edit history tracked and viewable
+
- [ ] Crossposting works across communities
+
- [ ] Save posts feature functional
+
+
### V1 Goals
+
- [ ] Bluesky posts display inline (federation)
+
- [ ] Vote fuzzing prevents manipulation
+
- [ ] Reputation affects vote weight
+
- [ ] Location-based posting for local communities
+
- [ ] Post templates reduce friction for common posts
+
+
---
+
+
## Related Documents
+
+
- [PRD_COMMUNITIES.md](PRD_COMMUNITIES.md) - Community system (posts require communities)
+
- [DOMAIN_KNOWLEDGE.md](DOMAIN_KNOWLEDGE.md) - Overall platform architecture
+
- [PRD_GOVERNANCE.md](PRD_GOVERNANCE.md) - Moderation and tagging systems
+
- **PRD_COMMENTS.md** (TODO) - Comment threading and replies
+
- **PRD_FEEDS.md** (TODO) - Feed generation and ranking algorithms
+
+
---
+
+
## Lexicon Summary
+
+
### `social.coves.post.record`
+
**Status:** ✅ Defined, implementation TODO
+
**Last Updated:** 2025-10-18 (removed `postType` enum)
+
+
**Required Fields:**
+
- `community` - DID of community (owner of repository)
+
- `createdAt` - Timestamp
+
+
**Optional Fields:**
+
- `title` - Post title (300 graphemes / 3000 bytes)
+
- `content` - Post content (50,000 characters max)
+
- `facets` - Rich text annotations
+
- `embed` - Images, video, external links, quoted posts (union type)
+
- `contentLabels` - Self-applied labels (nsfw, spoiler, violence)
+
- `originalAuthor` - For microblog posts (federated author info)
+
- `federatedFrom` - Reference to federated post
+
- `location` - Geographic coordinates
+
- `crosspostOf` - AT-URI of original post
+
- `crosspostChain` - Array of crosspost URIs
+
+
**Notes:**
+
- Author DID is inferred from the creation context (authenticated user), not stored in record
+
- Post "type" is derived from structure (has embed? what embed type? has title? text length?)
+
- Community's `contentRules` validate post structure at creation time
+
+
### `social.coves.post.create` (Procedure)
+
**Status:** ✅ Defined, implementation TODO
+
**Last Updated:** 2025-10-18 (removed `postType` parameter)
+
+
**Input Parameters:**
+
- `community` (required) - DID or handle of community to post in
+
- `title` (optional) - Post title
+
- `content` (optional) - Post content
+
- `facets` (optional) - Rich text annotations
+
- `embed` (optional) - Embedded content (images, video, external, post)
+
- `contentLabels` (optional) - Self-applied labels
+
- `originalAuthor` (optional) - For federated posts
+
- `federatedFrom` (optional) - Reference to federated post
+
- `location` (optional) - Geographic coordinates
+
+
**Validation:**
+
- Community exists and is accessible
+
- Post structure complies with community's `contentRules`
+
- Content within global limits (unless community sets stricter limits)
+
+
**Errors:**
+
- `CommunityNotFound` - Community doesn't exist
+
- `NotAuthorized` - User not authorized to post
+
- `Banned` - User is banned from community
+
- `InvalidContent` - Content violates general rules
+
- `ContentRuleViolation` - Post violates community's content rules
+
+
---
+
+
### `social.coves.interaction.vote`
+
**Status:** ✅ Defined, implementation TODO
+
+
**Fields:**
+
- `subject` - AT-URI of post/comment being voted on
+
- `createdAt` - Timestamp
+
+
**Note:** Direction (up/down) inferred from record creation/deletion pattern. Stored in user's repository (user owns votes).
+
+
### `social.coves.interaction.tag`
+
**Status:** ✅ Defined, deferred to Beta
+
+
**Fields:**
+
- `subject` - AT-URI of post/comment
+
- `tag` - Tag string (known values: helpful, insightful, spam, hostile, offtopic, misleading)
+
- `createdAt` - Timestamp
+
+
**Note:** Tags live in user's repository (users own their tags).
+
+
---
+
+
## References
+
+
- atProto Lexicon Spec: https://atproto.com/specs/lexicon
+
- atProto Repository Spec: https://atproto.com/specs/repository
+
- Bluesky Post Record: https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/post.json
+
- Rich Text Facets: https://atproto.com/specs/rich-text
+
- Coves V2 Communities Architecture: [PRD_COMMUNITIES.md](PRD_COMMUNITIES.md)