···
1
+
# Posts PRD: Forum Content System
3
+
**Status:** ✅ Alpha CREATE Complete (2025-10-19) | Get/Update/Delete/Voting TODO
4
+
**Owner:** Platform Team
5
+
**Last Updated:** 2025-10-19
7
+
## 🎯 Implementation Status
9
+
### ✅ COMPLETED (Alpha - 2025-10-19)
10
+
- **Post Creation:** Full write-forward to community PDS with real-time Jetstream indexing
11
+
- **Handler Layer:** HTTP endpoint with authentication, validation, and security checks
12
+
- **Service Layer:** Business logic with token refresh and community resolution
13
+
- **Repository Layer:** PostgreSQL storage with proper indexing
14
+
- **Jetstream Consumer:** Real-time indexing with security validation
15
+
- **Database Migration:** Posts table created (migration 011)
16
+
- **E2E Tests:** Live PDS + Jetstream integration tests passing
17
+
- **at-identifier Support:** All 4 formats (DIDs, canonical, @-prefixed, scoped handles)
19
+
### ⚠️ DEFERRED TO BETA
20
+
- Content rules validation (text-only, image-only communities)
21
+
- Post read operations (get, list)
22
+
- Post update/edit operations
24
+
- Voting system (upvotes/downvotes)
25
+
- Derived characteristics indexing (embed_type, text_length, etc.)
27
+
**See:** [IMPLEMENTATION_POST_CREATION.md](IMPLEMENTATION_POST_CREATION.md) for complete implementation details.
33
+
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.
37
+
### atProto Data Flow
38
+
Posts follow the community-owned repository pattern, matching the V2 Communities architecture:
41
+
User creates post → Written to COMMUNITY's PDS repository (using community credentials) →
42
+
Firehose broadcasts event → AppView Jetstream consumer indexes →
43
+
Post appears in feeds
46
+
**Repository Structure:**
48
+
Repository: at://did:plc:community789/social.coves.post.record/3k2a4b5c6d7e
49
+
Owner: did:plc:community789 (community owns the post)
50
+
Author: did:plc:user123 (tracked in record metadata)
51
+
Hosted By: did:web:coves.social (instance manages community credentials)
54
+
**Key Architectural Principles:**
55
+
- ✅ Communities own posts (posts live in community repos, like traditional forums)
56
+
- ✅ Author tracked in metadata (post.author field references user DID)
57
+
- ✅ Communities are portable (migrate instance = posts move with community)
58
+
- ✅ Matches V2 Communities pattern (community owns repository, instance manages credentials)
59
+
- ✅ Write operations use community's PDS credentials (not user credentials)
63
+
## Alpha Features (MVP - Ship First)
65
+
### Content Rules Integration
66
+
**Status:** Lexicon complete (2025-10-18), validation TODO
67
+
**Priority:** CRITICAL - Required for community content policies
69
+
Posts are validated against community-specific content rules at creation time. Communities can restrict:
70
+
- Allowed embed types (images, video, external, record)
71
+
- Text requirements (min/max length, required/optional)
72
+
- Title requirements
73
+
- Image count limits
74
+
- Federated content policies
76
+
**See:** [PRD_GOVERNANCE.md - Content Rules System](PRD_GOVERNANCE.md#content-rules-system) for full details.
78
+
**Implementation checklist:**
79
+
- [x] Lexicon: `contentRules` in `social.coves.community.profile` ✅
80
+
- [x] Lexicon: `postType` removed from `social.coves.post.create` ✅
81
+
- [ ] Validation: `ValidatePostAgainstRules()` service function
82
+
- [ ] Handler: Integrate validation in post creation endpoint
83
+
- [ ] AppView: Index derived characteristics (embed_type, text_length, etc.)
84
+
- [ ] Tests: Validate content rule enforcement
88
+
### Core Post Management
89
+
**Status:** ✅ CREATE COMPLETE (2025-10-19) - Get/Update/Delete TODO
90
+
**Priority:** CRITICAL - Posts are the foundation of the platform
93
+
- [x] Lexicon: `social.coves.post.record` ✅
94
+
- [x] Lexicon: `social.coves.post.create` ✅
95
+
- [x] Removed `postType` enum in favor of content rules ✅ (2025-10-18)
96
+
- [x] Removed `postType` from record and get lexicons ✅ (2025-10-18)
97
+
- [x] **Handler:** `POST /xrpc/social.coves.post.create` ✅ (Alpha - see IMPLEMENTATION_POST_CREATION.md)
98
+
- ✅ Accept: community (DID/handle), title (optional), content, facets, embed, contentLabels
99
+
- ✅ Validate: User is authenticated, community exists, content within limits
100
+
- ✅ Write: Create record in **community's PDS repository**
101
+
- ✅ Return: AT-URI and CID of created post
102
+
- ⚠️ Content rules validation deferred to Beta
103
+
- [x] **Service Layer:** `PostService.Create()` ✅
104
+
- ✅ Resolve community identifier to DID (supports all 4 at-identifier formats)
105
+
- ✅ Validate community exists and is not private
106
+
- ✅ Fetch community from AppView
107
+
- ⚠️ **Validate post against content rules** DEFERRED (see [PRD_GOVERNANCE.md](PRD_GOVERNANCE.md#content-rules-system))
108
+
- ✅ Fetch community's PDS credentials with automatic token refresh
109
+
- ✅ Build post record with author DID, timestamp, content
110
+
- ✅ **Write to community's PDS** using community's access token
111
+
- ✅ Return URI/CID for AppView indexing
112
+
- [x] **Validation:** ✅
113
+
- ✅ Community reference is valid (supports DIDs and handles)
114
+
- ✅ Content length ≤ 50,000 characters
115
+
- ✅ Title (if provided) ≤ 3,000 bytes
116
+
- ✅ ContentLabels are from known values (nsfw, spoiler, violence)
117
+
- ⚠️ **Content rules compliance:** DEFERRED TO BETA
118
+
- Check embed types against `allowedEmbedTypes`
119
+
- Verify `requireText` / `minTextLength` / `maxTextLength`
120
+
- Verify `requireTitle` if set
121
+
- Check image counts against `minImages` / `maxImages`
122
+
- Block federated posts if `allowFederated: false`
123
+
- Return `ContentRuleViolation` error if validation fails
124
+
- [x] **E2E Test:** Create text post → Write to **community's PDS** → Index via Jetstream → Verify in AppView ✅
127
+
- [x] Lexicon: `social.coves.post.get` ✅
128
+
- [ ] **Handler:** `GET /xrpc/social.coves.post.get?uri=at://...`
129
+
- Accept: AT-URI of post
130
+
- Return: Full post view with author, community, stats, viewer state
131
+
- [ ] **Service Layer:** `PostService.Get(uri, viewerDID)`
132
+
- Fetch post from AppView PostgreSQL
133
+
- Join with user/community data
134
+
- Calculate stats (upvotes, downvotes, score, comment count)
135
+
- Include viewer state (vote status, saved status, tags)
136
+
- [ ] **Repository:** `PostRepository.GetByURI()`
137
+
- Single query with JOINs for author, community, stats
138
+
- Handle missing posts gracefully (deleted or not indexed)
139
+
- [ ] **E2E Test:** Get post by URI → Verify all fields populated
142
+
- [x] Lexicon: `social.coves.post.update` ✅
143
+
- [ ] **Handler:** `POST /xrpc/social.coves.post.update`
144
+
- Accept: uri, title, content, facets, embed, contentLabels, editNote
145
+
- Validate: User is post author, within 24-hour edit window
146
+
- Write: Update record in **community's PDS**
148
+
- [ ] **Service Layer:** `PostService.Update()`
149
+
- Fetch existing post from AppView
150
+
- Verify authorship (post.author == authenticated user DID)
151
+
- Verify edit window (createdAt + 24 hours > now)
152
+
- Fetch community's PDS credentials (with token refresh)
153
+
- **Update record in community's PDS** using community's access token
154
+
- Track edit timestamp (editedAt field)
155
+
- [ ] **Edit Window:** 24 hours from creation (hardcoded for Alpha)
156
+
- [ ] **Edit Note:** Optional explanation field (stored in record)
157
+
- [ ] **E2E Test:** Update post → Verify edit reflected in AppView
160
+
- [x] Lexicon: `social.coves.post.delete` ✅
161
+
- [ ] **Handler:** `POST /xrpc/social.coves.post.delete`
163
+
- Validate: User is post author OR community moderator
164
+
- Write: Delete record from **community's PDS**
165
+
- [ ] **Service Layer:** `PostService.Delete()`
166
+
- Verify authorship OR moderator permission
167
+
- Fetch community's PDS credentials
168
+
- **Delete from community's PDS** (broadcasts DELETE event to firehose)
169
+
- Consumer handles soft delete in AppView
170
+
- [ ] **AppView Behavior:** Mark as deleted (soft delete), hide from feeds
171
+
- [ ] **Moderator Delete:** Community moderators can delete any post in their community
172
+
- [ ] **E2E Test:** Delete post → Verify hidden from queries
176
+
### Post Content Features
178
+
#### Rich Text Support
179
+
- [x] Lexicon: Facets reference `social.coves.richtext.facet` ✅
180
+
- [ ] **Supported Facets:**
181
+
- Mentions: `@user.bsky.social` → Links to user profile
182
+
- Links: `https://example.com` → Clickable URLs
183
+
- Community mentions: `!community@instance` → Links to community
184
+
- Hashtags: `#topic` → Tag-based discovery (Future)
185
+
- [ ] **Implementation:**
186
+
- Store facets as JSON array in post record
187
+
- Validate byte ranges match content
188
+
- Render facets in AppView responses
190
+
#### Embeds (Alpha Scope)
191
+
- [x] Lexicon: Embed union type ✅
192
+
- [ ] **Alpha Support:**
193
+
- **Images:** Upload to community's PDS blob storage, reference in embed
194
+
- **External Links:** URL, title, description, thumbnail (client-fetched)
195
+
- **Quoted Posts:** Reference another post's AT-URI
196
+
- [ ] **Defer to Beta:**
197
+
- Video embeds (requires video processing infrastructure)
199
+
#### Content Labels
200
+
- [x] Lexicon: Self-applied labels ✅
201
+
- [ ] **Alpha Labels:**
202
+
- `nsfw` - Not safe for work
203
+
- `spoiler` - Spoiler content (blur/hide by default)
204
+
- `violence` - Violent or graphic content
205
+
- [ ] **Implementation:**
206
+
- Store as string array in post record
207
+
- AppView respects labels in feed filtering
208
+
- Client renders appropriate warnings/blurs
214
+
#### Upvotes & Downvotes
215
+
- [x] Lexicon: `social.coves.interaction.vote` ✅
216
+
- [ ] **Handler:** `POST /xrpc/social.coves.interaction.createVote`
217
+
- Accept: subject (post AT-URI), direction (up/down)
218
+
- Write: Create vote record in **user's repository**
219
+
- [ ] **Handler:** `POST /xrpc/social.coves.interaction.deleteVote`
220
+
- Accept: voteUri (AT-URI of vote record)
221
+
- Write: Delete vote record from **user's repository**
222
+
- [ ] **Vote Toggling:**
223
+
- Upvote → Upvote = Delete upvote
224
+
- Upvote → Downvote = Delete upvote + Create downvote
225
+
- No vote → Upvote = Create upvote
226
+
- [ ] **Downvote Controls (Alpha):**
227
+
- Global default: Downvotes enabled
228
+
- Community-level toggle: `allowDownvotes` (Boolean in community.profile)
229
+
- Instance-level toggle: Environment variable `ALLOW_DOWNVOTES` (Future)
230
+
- [ ] **AppView Indexing:**
231
+
- Consumer tracks vote CREATE/DELETE events
232
+
- Aggregate counts: upvotes, downvotes, score (upvotes - downvotes)
233
+
- Track viewer's vote state (for "already voted" UI)
234
+
- [ ] **E2E Test:** Create vote → Index → Verify count updates → Delete vote → Verify count decrements
236
+
**Note:** Votes live in user's repository (user owns their voting history), but posts live in community's repository.
238
+
#### Vote Statistics
239
+
- [x] Lexicon: `postStats` in post view ✅
240
+
- [ ] **Stats Fields:**
241
+
- `upvotes` - Total upvote count
242
+
- `downvotes` - Total downvote count (0 if community disables)
243
+
- `score` - Calculated score (upvotes - downvotes)
244
+
- `commentCount` - Total comments (placeholder for Beta)
245
+
- `shareCount` - Share tracking (Future)
246
+
- `tagCounts` - Aggregate tag counts (Future)
250
+
### Jetstream Consumer (Indexing)
252
+
#### Post Event Handling
253
+
- [x] **Consumer:** `PostConsumer.HandlePostEvent()` ✅ (2025-10-19)
254
+
- ✅ Listen for `social.coves.post.record` CREATE from **community repositories**
255
+
- ✅ Parse post record, extract author DID and community DID (from AT-URI owner)
256
+
- ⚠️ **Derive post characteristics:** DEFERRED (embed_type, text_length, has_title, has_embed for content rules filtering)
257
+
- ✅ Insert in AppView PostgreSQL (CREATE only - UPDATE/DELETE deferred)
258
+
- ✅ Index: uri, cid, author_did, community_did, title, content, created_at, indexed_at
259
+
- ✅ **Security Validation:**
260
+
- ✅ Verify event.repo matches community DID (posts must come from community repos)
261
+
- ✅ Verify community exists in AppView (foreign key integrity)
262
+
- ✅ Verify author exists in AppView (foreign key integrity)
263
+
- ✅ Idempotent indexing for Jetstream replays
265
+
#### Vote Event Handling
266
+
- [ ] **Consumer:** `PostConsumer.HandleVoteEvent()` - DEFERRED TO BETA (voting system not yet implemented)
267
+
- Listen for `social.coves.interaction.vote` CREATE/DELETE from **user repositories**
268
+
- Parse subject URI (extract post)
269
+
- Increment/decrement vote counts atomically
270
+
- Track vote URI for viewer state queries
271
+
- **Validation:** Verify event.repo matches voter DID (votes must come from user repos)
273
+
#### Error Handling
274
+
- [x] Invalid community references → Reject post (foreign key enforcement) ✅
275
+
- [x] Invalid author references → Reject post (foreign key enforcement) ✅
276
+
- [x] Malformed records → Skip indexing, log error ✅
277
+
- [x] Duplicate events → Idempotent operations (unique constraint on URI) ✅
278
+
- [x] Posts from user repos → Reject (repository DID validation) ✅
282
+
### Database Schema
284
+
#### Posts Table ✅ IMPLEMENTED (2025-10-19)
286
+
**Migration:** [internal/db/migrations/011_create_posts_table.sql](../internal/db/migrations/011_create_posts_table.sql)
289
+
CREATE TABLE posts (
290
+
id BIGSERIAL PRIMARY KEY,
291
+
uri TEXT UNIQUE NOT NULL, -- AT-URI (at://community_did/collection/rkey)
292
+
cid TEXT NOT NULL, -- Content ID
293
+
rkey TEXT NOT NULL, -- Record key (TID)
294
+
author_did TEXT NOT NULL, -- Author's DID (from record metadata)
295
+
community_did TEXT NOT NULL, -- Community DID (from AT-URI owner)
296
+
title TEXT, -- Post title (nullable)
297
+
content TEXT, -- Post content
298
+
content_facets JSONB, -- Rich text facets
299
+
embed JSONB, -- Embedded content
300
+
content_labels TEXT[], -- Self-applied labels
302
+
-- ⚠️ Derived characteristics DEFERRED TO BETA (for content rules filtering)
303
+
-- Will be added when content rules are implemented:
304
+
-- embed_type TEXT, -- images, video, external, record (NULL if no embed)
305
+
-- text_length INT NOT NULL DEFAULT 0, -- Character count of content
306
+
-- has_title BOOLEAN NOT NULL DEFAULT FALSE,
307
+
-- has_embed BOOLEAN NOT NULL DEFAULT FALSE,
309
+
created_at TIMESTAMPTZ NOT NULL, -- Author's timestamp
310
+
edited_at TIMESTAMPTZ, -- Last edit timestamp
311
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When indexed
312
+
deleted_at TIMESTAMPTZ, -- Soft delete
314
+
-- Stats (denormalized for performance)
315
+
upvote_count INT NOT NULL DEFAULT 0,
316
+
downvote_count INT NOT NULL DEFAULT 0,
317
+
score INT NOT NULL DEFAULT 0, -- upvote_count - downvote_count
318
+
comment_count INT NOT NULL DEFAULT 0,
320
+
CONSTRAINT fk_author FOREIGN KEY (author_did) REFERENCES users(did) ON DELETE CASCADE,
321
+
CONSTRAINT fk_community FOREIGN KEY (community_did) REFERENCES communities(did) ON DELETE CASCADE
324
+
-- ✅ Implemented indexes
325
+
CREATE INDEX idx_posts_community_created ON posts(community_did, created_at DESC) WHERE deleted_at IS NULL;
326
+
CREATE INDEX idx_posts_community_score ON posts(community_did, score DESC, created_at DESC) WHERE deleted_at IS NULL;
327
+
CREATE INDEX idx_posts_author ON posts(author_did, created_at DESC);
328
+
CREATE INDEX idx_posts_uri ON posts(uri);
330
+
-- ⚠️ Deferred until content rules are implemented:
331
+
-- CREATE INDEX idx_posts_embed_type ON posts(community_did, embed_type) WHERE deleted_at IS NULL;
336
+
CREATE TABLE votes (
337
+
id BIGSERIAL PRIMARY KEY,
338
+
uri TEXT UNIQUE NOT NULL, -- Vote record AT-URI (at://voter_did/collection/rkey)
340
+
rkey TEXT NOT NULL,
341
+
voter_did TEXT NOT NULL, -- User who voted (from AT-URI owner)
342
+
subject_uri TEXT NOT NULL, -- Post/comment AT-URI
343
+
direction TEXT NOT NULL CHECK (direction IN ('up', 'down')),
344
+
created_at TIMESTAMPTZ NOT NULL,
345
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
346
+
deleted_at TIMESTAMPTZ,
348
+
CONSTRAINT fk_voter FOREIGN KEY (voter_did) REFERENCES users(did),
349
+
UNIQUE (voter_did, subject_uri, deleted_at) -- One active vote per user per subject
352
+
CREATE INDEX idx_votes_subject ON votes(subject_uri, direction) WHERE deleted_at IS NULL;
353
+
CREATE INDEX idx_votes_voter_subject ON votes(voter_did, subject_uri) WHERE deleted_at IS NULL;
358
+
## Alpha Blockers (Must Complete)
361
+
- [x] **Post CREATE Endpoint:** ✅ COMPLETE (2025-10-19)
362
+
- ✅ Handler with authentication and validation
363
+
- ✅ Service layer with business logic
364
+
- ✅ Repository layer for database access
365
+
- ⚠️ Get, Update, Delete deferred to Beta
366
+
- [x] **Community Credentials:** ✅ Use community's PDS credentials for post writes
367
+
- [x] **Token Refresh Integration:** ✅ Reuse community token refresh logic for post operations
368
+
- [x] **Jetstream Consumer:** ✅ Posts indexed in real-time (2025-10-19)
369
+
- ✅ CREATE operations indexed
370
+
- ✅ Security validation (repository ownership)
371
+
- ⚠️ UPDATE/DELETE deferred until those features exist
372
+
- ⚠️ Derived characteristics deferred to Beta
373
+
- [x] **Database Migrations:** ✅ Posts table created (migration 011)
374
+
- [x] **E2E Tests:** ✅ Full flow tested (handler → community PDS → Jetstream → AppView)
375
+
- ✅ Service layer tests (9 subtests)
376
+
- ✅ Repository tests (2 subtests)
377
+
- ✅ Handler security tests (10+ subtests)
378
+
- ✅ Live PDS + Jetstream E2E test
379
+
- [x] **Community Integration:** ✅ Posts correctly reference communities via at-identifiers
380
+
- [x] **at-identifier Support:** ✅ All 4 formats supported (DIDs, canonical, @-prefixed, scoped)
381
+
- [ ] **Content Rules Validation:** ⚠️ DEFERRED TO BETA - Posts validated against community content rules
382
+
- [ ] **Vote System:** ⚠️ DEFERRED TO BETA - Upvote/downvote with community-level controls
383
+
- [ ] **Moderator Permissions:** ⚠️ DEFERRED TO BETA - Community moderators can delete posts
385
+
### Testing Requirements
386
+
- [x] Create text post in community → Appears in AppView ✅
387
+
- [x] Post lives in community's repository (verify AT-URI owner) ✅
388
+
- [x] Post written to PDS → Broadcast to Jetstream → Indexed in AppView ✅
389
+
- [x] Handler security: Rejects client-provided authorDid ✅
390
+
- [x] Handler security: Requires authentication ✅
391
+
- [x] Handler security: Validates request body size ✅
392
+
- [x] Handler security: All 4 at-identifier formats accepted ✅
393
+
- [x] Consumer security: Rejects posts from wrong repository ✅
394
+
- [x] Consumer security: Verifies community and author exist ✅
395
+
- [ ] **Content rules validation:** Text-only community rejects image posts ⚠️ DEFERRED
396
+
- [ ] **Content rules validation:** Image community rejects posts without images ⚠️ DEFERRED
397
+
- [ ] **Content rules validation:** Post with too-short text rejected ⚠️ DEFERRED
398
+
- [ ] **Content rules validation:** Federated post rejected if `allowFederated: false` ⚠️ DEFERRED
399
+
- [ ] Update post within 24 hours → Edit reflected ⚠️ DEFERRED
400
+
- [ ] Delete post as author → Hidden from queries ⚠️ DEFERRED
401
+
- [ ] Delete post as moderator → Hidden from queries ⚠️ DEFERRED
402
+
- [ ] Upvote post → Count increments ⚠️ DEFERRED
403
+
- [ ] Downvote post → Count increments (if enabled) ⚠️ DEFERRED
404
+
- [ ] Toggle vote → Counts update correctly ⚠️ DEFERRED
405
+
- [ ] Community with downvotes disabled → Downvote returns error ⚠️ DEFERRED
409
+
## Beta Features (Post-Alpha)
411
+
### Advanced Post Types
412
+
**Status:** Deferred - Simplify for Alpha
413
+
**Rationale:** Text posts are sufficient for MVP, other types need more infrastructure
415
+
- [ ] **Image Posts:** Full image upload/processing pipeline
416
+
- Multi-image support (up to 4 images)
417
+
- Upload to community's PDS blob storage
418
+
- Thumbnail generation
419
+
- Alt text requirements (accessibility)
421
+
- [ ] **Video Posts:** Video hosting and processing
422
+
- Video upload to community's PDS blob storage
423
+
- Thumbnail extraction
424
+
- Format validation
425
+
- Streaming support
427
+
- [ ] **Microblog Posts:** Bluesky federation integration
428
+
- Fetch Bluesky posts by AT-URI
429
+
- Display inline with native posts
430
+
- Track original author info
431
+
- Federation metadata
433
+
- [ ] **Decision Point:** Remove "Article" type entirely?
434
+
- Obsoleted by planned RSS aggregation service
435
+
- LLMs will break down articles into digestible content
436
+
- May not need native article posting
438
+
### Post Interaction Features
440
+
#### Tagging System
441
+
- [x] Lexicon: `social.coves.interaction.tag` ✅
442
+
- [ ] **Known Tags:** helpful, insightful, spam, hostile, offtopic, misleading
443
+
- [ ] **Community Custom Tags:** Communities define their own tags
444
+
- [ ] **Aggregate Counts:** Track tag distribution on posts
445
+
- [ ] **Moderation Integration:** High spam/hostile tags trigger tribunal review
446
+
- [ ] **Reputation Impact:** Helpful/insightful tags boost author reputation
447
+
- [ ] **Tag Storage:** Tags live in **user's repository** (users own their tags)
450
+
- [x] Lexicon: `social.coves.post.crosspost` ✅
451
+
- [ ] **Crosspost Tracking:** Share post to multiple communities
452
+
- [ ] **Implementation:** Create new post record in each community's repository
453
+
- [ ] **Crosspost Chain:** Track all crosspost relationships
454
+
- [ ] **Deduplication:** Show original + crosspost count (don't spam feeds)
455
+
- [ ] **Rules:** Communities can disable crossposting
458
+
- [ ] **Lexicon:** Create `social.coves.actor.savedPost` record type
459
+
- [ ] **Functionality:** Bookmark posts for later reading
460
+
- [ ] **Private List:** Saved posts stored in **user's repository**
461
+
- [ ] **AppView Query:** Endpoint to fetch user's saved posts
464
+
- [x] Lexicon: `social.coves.post.search` ✅
465
+
- [ ] **Search Parameters:**
467
+
- Filter by community
469
+
- Filter by post type
471
+
- Sort: relevance, new, top
472
+
- Timeframe: hour, day, week, month, year, all
473
+
- [ ] **Implementation:**
474
+
- PostgreSQL full-text search (tsvector on title + content)
475
+
- Relevance ranking algorithm
476
+
- Pagination with cursor
479
+
- [ ] **Track Edits:** Store edit history in AppView (not in atProto record)
480
+
- [ ] **Edit Diff:** Show what changed between versions
481
+
- [ ] **Edit Log:** List all edits with timestamps and edit notes
482
+
- [ ] **Revision Viewing:** View previous versions of post
484
+
### Advanced Voting
486
+
#### Vote Weight by Reputation
487
+
- [ ] **Reputation Multiplier:** High-reputation users' votes count more
488
+
- [ ] **Community-Specific:** Reputation calculated per-community
489
+
- [ ] **Transparency:** Show vote weight in moderation logs (not public)
491
+
#### Fuzzing & Vote Obfuscation
492
+
- [ ] **Count Fuzzing:** Add noise to vote counts (prevent manipulation detection)
493
+
- [ ] **Delay Display:** Don't show exact counts for new posts (first hour)
494
+
- [ ] **Rate Limiting:** Prevent vote brigading
502
+
#### Bluesky Integration
503
+
- [ ] **Display Bluesky Posts:** Show Bluesky posts in community feeds (microblog type)
504
+
- [ ] **Original Author Info:** Track Bluesky user metadata
505
+
- [ ] **No Native Commenting:** Users see Bluesky posts, can't comment (yet)
506
+
- [ ] **Reference Storage:** Store Bluesky AT-URI, don't duplicate content
508
+
#### ActivityPub Integration
509
+
- [ ] **Lemmy/Mbin Posts:** Convert ActivityPub posts to Coves posts
510
+
- [ ] **Bidirectional Sync:** Coves posts appear on Lemmy instances
511
+
- [ ] **User Identity Mapping:** Assign DIDs to ActivityPub users
512
+
- [ ] **Action Translation:** Upvotes ↔ ActivityPub likes
514
+
### Advanced Features
516
+
#### Post Scheduling
517
+
- [ ] Schedule posts for future publishing
518
+
- [ ] Edit scheduled posts before they go live
519
+
- [ ] Cancel scheduled posts
521
+
#### Post Templates
522
+
- [ ] Communities define post templates
523
+
- [ ] Auto-fill fields for common post types
524
+
- [ ] Game threads, event announcements, etc.
527
+
- [ ] Create polls in posts
528
+
- [ ] Multiple choice, ranked choice, approval voting
529
+
- [ ] Time-limited voting windows
530
+
- [ ] Results visualization
532
+
#### Location-Based Posting
533
+
- [x] Lexicon: `location` field in post record ✅
534
+
- [ ] **Geo-Tagging:** Attach coordinates to posts
535
+
- [ ] **Community Rules:** Require location for certain posts (local events)
536
+
- [ ] **Privacy:** User controls location precision
537
+
- [ ] **Discovery:** Filter posts by location
541
+
## Technical Decisions Log
543
+
### 2025-10-18: Content Rules Over Post Type Enum
544
+
**Decision:** Remove `postType` from post creation input; validate posts against community's `contentRules` instead
547
+
- `postType` enum forced users to explicitly select type (bad UX - app should infer from structure)
548
+
- Structure-based validation is more flexible ("text required, images optional" vs rigid type categories)
549
+
- Content rules are extensible without changing post lexicon
550
+
- Enables both community restrictions (governance) AND user filtering (UI preferences)
551
+
- Follows atProto philosophy: describe data structure, not UI intent
553
+
**Implementation:**
554
+
- Post creation no longer accepts `postType` parameter
555
+
- Community profile contains optional `contentRules` object
556
+
- Handler validates post structure against community's content rules
557
+
- AppView indexes derived characteristics (embed_type, text_length, has_title, has_embed)
558
+
- Validation error changed from `InvalidPostType` to `ContentRuleViolation`
560
+
**Database Changes:**
561
+
- Remove `post_type` enum column
562
+
- Add derived fields: `embed_type`, `text_length`, `has_title`, `has_embed`
563
+
- Add index on `embed_type` for filtering
566
+
- Text-only community: `allowedEmbedTypes: []` + `requireText: true`
567
+
- Image community: `allowedEmbedTypes: ["images"]` + `minImages: 1`
568
+
- No restrictions: `contentRules: null`
570
+
**See:** [PRD_GOVERNANCE.md - Content Rules System](PRD_GOVERNANCE.md#content-rules-system)
574
+
### 2025-10-18: Posts Live in Community Repositories
575
+
**Decision:** Posts are stored in community's repository, not user's repository
578
+
- **Matches V2 Communities Architecture:** Communities own their repositories
579
+
- **Traditional Forum Model:** Community owns content, author tracked in metadata
580
+
- **Simpler Permissions:** Use community credentials for all post writes
581
+
- **Portability:** Posts migrate with community when changing instances
582
+
- **Moderation:** Community has full control over content
583
+
- **Reuses Token Refresh:** Can leverage existing community credential management
585
+
**Implementation Details:**
586
+
- Post AT-URI: `at://community_did/social.coves.post.record/tid`
587
+
- Write operations use community's PDS credentials (encrypted, stored in AppView)
588
+
- Author tracked in post record's `author` field (DID)
589
+
- Moderators can delete any post in their community
590
+
- Token refresh reuses community's refresh logic
592
+
**Trade-offs vs User-Owned Posts:**
593
+
- ❌ Users can't take posts when leaving community/instance
594
+
- ❌ Less "web3" (content not user-owned)
595
+
- ✅ Traditional forum UX (users expect community to own content)
596
+
- ✅ Simpler implementation (one credential store per community)
597
+
- ✅ Easier moderation (community has full control)
598
+
- ✅ Posts move with community during migration
600
+
**Comparison to Bluesky:**
601
+
- Bluesky: Users own posts (posts in user repo)
602
+
- Coves: Communities own posts (posts in community repo)
603
+
- This is acceptable - different platforms, different models
604
+
- Still atProto-compliant (just different ownership pattern)
608
+
### 2025-10-18: Votes Live in User Repositories
609
+
**Decision:** Vote records are stored in user's repository, not community's
612
+
- Users own their voting history (personal preference)
613
+
- Matches Bluesky pattern (likes in user's repo)
614
+
- Enables portable voting history across instances
615
+
- User controls their own voting record
617
+
**Implementation Details:**
618
+
- Vote AT-URI: `at://user_did/social.coves.interaction.vote/tid`
619
+
- Write operations use user's PDS credentials
620
+
- Subject field references post AT-URI (in community's repo)
621
+
- Consumer aggregates votes from all users into post stats
625
+
### 2025-10-18: Simplify Post Types for Alpha
626
+
**Decision:** Launch with text posts only, defer other embed types to Beta
627
+
**Status:** SUPERSEDED by content rules approach (see above)
630
+
- Text posts are sufficient for forum discussions (core use case)
631
+
- Image/video embeds require additional infrastructure (blob storage, processing)
632
+
- Article format can be handled with long-form text posts
633
+
- Microblog type is for Bluesky federation (not immediate priority)
634
+
- Simplicity accelerates alpha launch
636
+
**Updated Approach (2025-10-18):**
637
+
- Post structure determines "type" (not explicit enum)
638
+
- Communities use `contentRules` to restrict embed types
639
+
- AppView derives `embed_type` from post structure for filtering
640
+
- More flexible than rigid type system
644
+
### 2025-10-18: Include Downvotes with Community Controls
645
+
**Decision:** Support both upvotes and downvotes, with toggles to disable downvotes
648
+
- Downvotes provide valuable signal for content quality
649
+
- Some communities prefer upvote-only (toxic negativity concerns)
650
+
- Instance operators should have global control option
651
+
- Reddit/HN have proven downvotes work with good moderation
653
+
**Implementation:**
654
+
- Community-level: `allowDownvotes` boolean in community profile
655
+
- Instance-level: Environment variable `ALLOW_DOWNVOTES` (future)
656
+
- Downvote attempts on disabled communities return error
657
+
- Stats show 0 downvotes when disabled
661
+
### 2025-10-18: 24-Hour Edit Window (Hardcoded for Alpha)
662
+
**Decision:** Posts can be edited for 24 hours after creation
665
+
- Allows fixing typos and errors
666
+
- Prevents historical revisionism (can't change old posts)
667
+
- 24 hours balances flexibility with integrity
668
+
- Future: Community-configurable edit windows
670
+
**Future Enhancements:**
671
+
- Edit history tracking (show what changed)
672
+
- Community-specific edit windows (0-72 hours)
673
+
- Moderator override (edit any post)
677
+
### 2025-10-18: Comments Separate from Posts PRD
678
+
**Decision:** Comments get their own dedicated PRD
681
+
- Comments are complex enough to warrant separate planning
682
+
- Threaded replies, vote inheritance, moderation all need design
683
+
- Posts are usable without comments (voting, tagging still work)
684
+
- Allows shipping posts sooner
686
+
**Scope Boundary:**
687
+
- **Posts PRD:** Post CRUD, voting, tagging, search
688
+
- **Comments PRD:** Comment threads, reply depth, sorting, moderation
692
+
### 2025-10-18: Feeds Separate from Posts PRD
693
+
**Decision:** Feed generation gets its own PRD
696
+
- Feed algorithms are complex (ranking, personalization, filtering)
697
+
- Posts need to exist before feeds can be built
698
+
- Feed work includes: Home feed, Community feed, All feed, read state tracking
699
+
- Allows iterating on feed algorithms independently
701
+
**Scope Boundary:**
702
+
- **Posts PRD:** Post creation, indexing, retrieval
703
+
- **Feeds PRD:** Feed generation, ranking algorithms, read state, personalization
709
+
### Alpha Launch Checklist ✅ COMPLETE (2025-10-19)
710
+
- [x] Users can create text posts in communities ✅
711
+
- [x] Posts are stored in community's repository (verify AT-URI) ✅
712
+
- [x] Posts use community's PDS credentials for writes ✅
713
+
- [x] Posts are indexed from firehose within 1 second ✅ (real-time Jetstream)
714
+
- [x] E2E tests cover full write-forward flow ✅
715
+
- [x] Database handles posts without performance issues ✅
716
+
- [x] Handler security tests passing (authentication, validation, body size) ✅
717
+
- [x] Consumer security validation (repository ownership, community/author checks) ✅
718
+
- [x] All 4 at-identifier formats supported ✅
720
+
### Beta Checklist (TODO)
721
+
- [ ] Post editing works within 24-hour window ⚠️ DEFERRED
722
+
- [ ] Upvote/downvote system functional ⚠️ DEFERRED
723
+
- [ ] Community downvote toggle works ⚠️ DEFERRED
724
+
- [ ] Post deletion soft-deletes and hides from queries ⚠️ DEFERRED
725
+
- [ ] Moderators can delete posts in their community ⚠️ DEFERRED
726
+
- [ ] Get post endpoint returns full post view with stats ⚠️ DEFERRED
727
+
- [ ] Content rules validation working ⚠️ DEFERRED
728
+
- [ ] Database handles 100,000+ posts (load testing)
731
+
- [ ] All post types supported (text, image, video, microblog)
732
+
- [ ] Tagging system enables community moderation
733
+
- [ ] Post search returns relevant results
734
+
- [ ] Edit history tracked and viewable
735
+
- [ ] Crossposting works across communities
736
+
- [ ] Save posts feature functional
739
+
- [ ] Bluesky posts display inline (federation)
740
+
- [ ] Vote fuzzing prevents manipulation
741
+
- [ ] Reputation affects vote weight
742
+
- [ ] Location-based posting for local communities
743
+
- [ ] Post templates reduce friction for common posts
747
+
## Related Documents
749
+
- [PRD_COMMUNITIES.md](PRD_COMMUNITIES.md) - Community system (posts require communities)
750
+
- [DOMAIN_KNOWLEDGE.md](DOMAIN_KNOWLEDGE.md) - Overall platform architecture
751
+
- [PRD_GOVERNANCE.md](PRD_GOVERNANCE.md) - Moderation and tagging systems
752
+
- **PRD_COMMENTS.md** (TODO) - Comment threading and replies
753
+
- **PRD_FEEDS.md** (TODO) - Feed generation and ranking algorithms
759
+
### `social.coves.post.record`
760
+
**Status:** ✅ Defined, implementation TODO
761
+
**Last Updated:** 2025-10-18 (removed `postType` enum)
763
+
**Required Fields:**
764
+
- `community` - DID of community (owner of repository)
765
+
- `createdAt` - Timestamp
767
+
**Optional Fields:**
768
+
- `title` - Post title (300 graphemes / 3000 bytes)
769
+
- `content` - Post content (50,000 characters max)
770
+
- `facets` - Rich text annotations
771
+
- `embed` - Images, video, external links, quoted posts (union type)
772
+
- `contentLabels` - Self-applied labels (nsfw, spoiler, violence)
773
+
- `originalAuthor` - For microblog posts (federated author info)
774
+
- `federatedFrom` - Reference to federated post
775
+
- `location` - Geographic coordinates
776
+
- `crosspostOf` - AT-URI of original post
777
+
- `crosspostChain` - Array of crosspost URIs
780
+
- Author DID is inferred from the creation context (authenticated user), not stored in record
781
+
- Post "type" is derived from structure (has embed? what embed type? has title? text length?)
782
+
- Community's `contentRules` validate post structure at creation time
784
+
### `social.coves.post.create` (Procedure)
785
+
**Status:** ✅ Defined, implementation TODO
786
+
**Last Updated:** 2025-10-18 (removed `postType` parameter)
788
+
**Input Parameters:**
789
+
- `community` (required) - DID or handle of community to post in
790
+
- `title` (optional) - Post title
791
+
- `content` (optional) - Post content
792
+
- `facets` (optional) - Rich text annotations
793
+
- `embed` (optional) - Embedded content (images, video, external, post)
794
+
- `contentLabels` (optional) - Self-applied labels
795
+
- `originalAuthor` (optional) - For federated posts
796
+
- `federatedFrom` (optional) - Reference to federated post
797
+
- `location` (optional) - Geographic coordinates
800
+
- Community exists and is accessible
801
+
- Post structure complies with community's `contentRules`
802
+
- Content within global limits (unless community sets stricter limits)
805
+
- `CommunityNotFound` - Community doesn't exist
806
+
- `NotAuthorized` - User not authorized to post
807
+
- `Banned` - User is banned from community
808
+
- `InvalidContent` - Content violates general rules
809
+
- `ContentRuleViolation` - Post violates community's content rules
813
+
### `social.coves.interaction.vote`
814
+
**Status:** ✅ Defined, implementation TODO
817
+
- `subject` - AT-URI of post/comment being voted on
818
+
- `createdAt` - Timestamp
820
+
**Note:** Direction (up/down) inferred from record creation/deletion pattern. Stored in user's repository (user owns votes).
822
+
### `social.coves.interaction.tag`
823
+
**Status:** ✅ Defined, deferred to Beta
826
+
- `subject` - AT-URI of post/comment
827
+
- `tag` - Tag string (known values: helpful, insightful, spam, hostile, offtopic, misleading)
828
+
- `createdAt` - Timestamp
830
+
**Note:** Tags live in user's repository (users own their tags).
836
+
- atProto Lexicon Spec: https://atproto.com/specs/lexicon
837
+
- atProto Repository Spec: https://atproto.com/specs/repository
838
+
- Bluesky Post Record: https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/post.json
839
+
- Rich Text Facets: https://atproto.com/specs/rich-text
840
+
- Coves V2 Communities Architecture: [PRD_COMMUNITIES.md](PRD_COMMUNITIES.md)