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) - Governence
- 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 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.community.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 for full details.
Implementation checklist:
- Lexicon:
contentRulesinsocial.coves.community.profile✅ - Lexicon:
postTyperemoved fromsocial.coves.community.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#
- Lexicon:
social.coves.community.post.record✅ - Lexicon:
social.coves.community.post.create✅ - Removed
postTypeenum in favor of content rules ✅ (2025-10-18) - Removed
postTypefrom record and get lexicons ✅ (2025-10-18) - Handler:
POST /xrpc/social.coves.community.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
- 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)
- ✅ 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
- 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
requireTitleif set - Check image counts against
minImages/maxImages - Block federated posts if
allowFederated: false - Return
ContentRuleViolationerror if validation fails
- Check embed types against
- E2E Test: Create text post → Write to community's PDS → Index via Jetstream → Verify in AppView ✅
Get Post#
- Lexicon:
social.coves.community.post.get✅ - Handler:
GET /xrpc/social.coves.community.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#
- Lexicon:
social.coves.community.post.update✅ - Handler:
POST /xrpc/social.coves.community.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#
- Lexicon:
social.coves.community.post.delete✅ - Handler:
POST /xrpc/social.coves.community.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#
- 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)
- Mentions:
- Implementation:
- Store facets as JSON array in post record
- Validate byte ranges match content
- Render facets in AppView responses
Embeds (Alpha Scope)#
- 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#
- Lexicon: Self-applied labels ✅
- Alpha Labels:
nsfw- Not safe for workspoiler- 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#
- 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#
- Lexicon:
postStatsin post view ✅ - Stats Fields:
upvotes- Total upvote countdownvotes- 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#
- Consumer:
PostConsumer.HandlePostEvent()✅ (2025-10-19)- ✅ Listen for
social.coves.community.post.recordCREATE 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
- ✅ Listen for
Vote Event Handling#
- Consumer:
PostConsumer.HandleVoteEvent()- DEFERRED TO BETA (voting system not yet implemented)- Listen for
social.coves.interaction.voteCREATE/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)
- Listen for
Error Handling#
- Invalid community references → Reject post (foreign key enforcement) ✅
- Invalid author references → Reject post (foreign key enforcement) ✅
- Malformed records → Skip indexing, log error ✅
- Duplicate events → Idempotent operations (unique constraint on URI) ✅
- 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
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#
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#
- 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
- Community Credentials: ✅ Use community's PDS credentials for post writes
- Token Refresh Integration: ✅ Reuse community token refresh logic for post operations
- 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
- Database Migrations: ✅ Posts table created (migration 011)
- 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
- Community Integration: ✅ Posts correctly reference communities via at-identifiers
- 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: - Upvote/downvote with community-level controls
- Moderator Permissions: ⚠️ DEFERRED TO BETA - Community moderators can delete posts
Testing Requirements#
- Create text post in community → Appears in AppView ✅
- Post lives in community's repository (verify AT-URI owner) ✅
- Post written to PDS → Broadcast to Jetstream → Indexed in AppView ✅
- Handler security: Rejects client-provided authorDid ✅
- Handler security: Requires authentication ✅
- Handler security: Validates request body size ✅
- Handler security: All 4 at-identifier formats accepted ✅
- Consumer security: Rejects posts from wrong repository ✅
- Consumer security: Verifies community and author exist ✅
- Content rules validation: Text-only community rejects image posts ⚠️ DEFERRED - Governence
- Content rules validation: Image community rejects posts without images ⚠️ DEFERRED - Governence
- Content rules validation: Post with too-short text rejected ⚠️ DEFERRED - Governence
- Content rules validation: Federated post rejected if
allowFederated: false⚠️ DEFERRED - Governence - 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
- Downvote post → Count increments (if enabled)
- Toggle vote → Counts update correctly ⚠️ DEFERRED - Governence
- Community with downvotes disabled → Downvote returns error ⚠️ DEFERRED - Governence
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#
- 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#
- Lexicon:
social.coves.community.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.savedPostrecord 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#
- Lexicon:
social.coves.community.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#
- Lexicon:
locationfield 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:
postTypeenum 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
postTypeparameter - Community profile contains optional
contentRulesobject - 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
InvalidPostTypetoContentRuleViolation
Database Changes:
- Remove
post_typeenum column - Add derived fields:
embed_type,text_length,has_title,has_embed - Add index on
embed_typefor 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
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.community.post.record/tid - Write operations use community's PDS credentials (encrypted, stored in AppView)
- Author tracked in post record's
authorfield (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
contentRulesto restrict embed types - AppView derives
embed_typefrom 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:
allowDownvotesboolean 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)#
- Users can create text posts in communities ✅
- Posts are stored in community's repository (verify AT-URI) ✅
- Posts use community's PDS credentials for writes ✅
- Posts are indexed from firehose within 1 second ✅ (real-time Jetstream)
- E2E tests cover full write-forward flow ✅
- Database handles posts without performance issues ✅
- Handler security tests passing (authentication, validation, body size) ✅
- Consumer security validation (repository ownership, community/author checks) ✅
- 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 - Community system (posts require communities)
- DOMAIN_KNOWLEDGE.md - Overall platform architecture
- 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.community.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 annotationsembed- 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 postlocation- Geographic coordinatescrosspostOf- AT-URI of original postcrosspostChain- 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
contentRulesvalidate post structure at creation time
social.coves.community.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 intitle(optional) - Post titlecontent(optional) - Post contentfacets(optional) - Rich text annotationsembed(optional) - Embedded content (images, video, external, post)contentLabels(optional) - Self-applied labelsoriginalAuthor(optional) - For federated postsfederatedFrom(optional) - Reference to federated postlocation(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 existNotAuthorized- User not authorized to postBanned- User is banned from communityInvalidContent- Content violates general rulesContentRuleViolation- Post violates community's content rules
social.coves.interaction.vote#
Status: ✅ Defined, implementation TODO
Fields:
subject- AT-URI of post/comment being voted oncreatedAt- 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/commenttag- 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