A community based topic aggregation platform built on atproto
1# Federation PRD: Cross-Instance Posting (Beta) 2 3**Status:** Planning - Beta 4**Target:** Beta Release 5**Owner:** TBD 6**Last Updated:** 2025-11-16 7 8--- 9 10## Overview 11 12Enable Lemmy-style federation where users on any Coves instance can post to communities hosted on other instances, while maintaining community ownership and moderation control. 13 14### Problem Statement 15 16**Current (Alpha):** 17- Posts to communities require community credentials 18- Users can only post to communities on their home instance 19- No true federation across instances 20 21**Desired (Beta):** 22- User A@coves.social can post to !gaming@covesinstance.com 23- Communities maintain full moderation control 24- Content lives in community repositories (not user repos) 25- Seamless UX - users don't think about federation 26 27--- 28 29## Goals 30 31### Primary Goals 321. **Enable cross-instance posting** - Users can post to any community on any federated instance 332. **Preserve community ownership** - Posts live in community repos, not user repos 343. **atProto-native implementation** - Use `com.atproto.server.getServiceAuth` pattern 354. **Maintain security** - No compromise on auth, validation, or moderation 36 37### Non-Goals (Future Versions) 38- Automatic instance discovery (Beta: manual allowlist) 39- Cross-instance moderation delegation 40- Content mirroring/replication 41- User migration between instances 42 43--- 44 45## Technical Approach 46 47### Architecture: atProto Service Auth 48 49Use atProto's native service authentication delegation pattern: 50 51``` 52┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ 53│ User A │ │ coves.social │ │ covesinstance│ 54│ @coves.soc │────────▶│ AppView │────────▶│ .com PDS │ 55└─────────────┘ (1) └──────────────────┘ (2) └─────────────┘ 56 JWT auth Request Service Auth Validate 57 │ │ 58 │◀──────────────────────┘ 59 │ (3) Scoped Token 60 61 62 ┌──────────────────┐ 63 │ covesinstance │ 64 │ .com PDS │ 65 │ Write Post │ 66 └──────────────────┘ 67 68 69 ┌──────────────────┐ 70 │ Firehose │ 71 │ (broadcasts) │ 72 └──────────────────┘ 73 74 ┌────────────┴────────────┐ 75 ▼ ▼ 76 ┌──────────────┐ ┌──────────────┐ 77 │ coves.social │ │covesinstance │ 78 │ AppView │ │ .com AppView│ 79 │ (indexes) │ │ (indexes) │ 80 └──────────────┘ └──────────────┘ 81``` 82 83### Flow Breakdown 84 85**Step 1: User Authentication (Unchanged)** 86- User authenticates with their home instance (coves.social) 87- Receives JWT token for API requests 88 89**Step 2: Service Auth Request (New)** 90- When posting to remote community, AppView requests service auth token 91- Endpoint: `POST {remote-pds}/xrpc/com.atproto.server.getServiceAuth` 92- Payload: 93 ```json 94 { 95 "aud": "did:plc:community123", // Community DID 96 "exp": 1234567890, // Token expiration 97 "lxm": "social.coves.community.post.create" // Authorized method 98 } 99 ``` 100 101**Step 3: Service Auth Validation (New - PDS Side)** 102- Remote PDS validates request: 103 - Is requesting service trusted? (instance allowlist) 104 - Is user banned from community? 105 - Does community allow remote posts? 106 - Rate limiting checks 107- Returns scoped token valid for specific community + operation 108 109**Step 4: Post Creation (Modified)** 110- AppView uses service auth token to write to remote PDS 111- Same `com.atproto.repo.createRecord` endpoint as current implementation 112- Post record written to community's repository 113 114**Step 5: Indexing (Unchanged)** 115- PDS broadcasts to firehose 116- All AppViews index via Jetstream consumers 117 118--- 119 120## Implementation Details 121 122### Phase 1: Service Detection (Local vs Remote) 123 124**File:** `internal/core/posts/service.go` 125 126```go 127func (s *postService) CreatePost(ctx context.Context, req CreatePostRequest) (*CreatePostResponse, error) { 128 // ... existing validation ... 129 130 community, err := s.communityService.GetByDID(ctx, communityDID) 131 if err != nil { 132 return nil, err 133 } 134 135 // NEW: Route based on community location 136 if s.isLocalCommunity(community) { 137 return s.createLocalPost(ctx, community, req) 138 } 139 return s.createFederatedPost(ctx, community, req) 140} 141 142func (s *postService) isLocalCommunity(community *communities.Community) bool { 143 localPDSHost := extractHost(s.pdsURL) 144 communityPDSHost := extractHost(community.PDSURL) 145 return localPDSHost == communityPDSHost 146} 147``` 148 149### Phase 2: Service Auth Client 150 151**New File:** `internal/atproto/service_auth/client.go` 152 153```go 154type ServiceAuthClient interface { 155 // RequestServiceAuth obtains a scoped token for writing to remote community 156 RequestServiceAuth(ctx context.Context, opts ServiceAuthOptions) (*ServiceAuthToken, error) 157} 158 159type ServiceAuthOptions struct { 160 RemotePDSURL string // Remote PDS endpoint 161 CommunityDID string // Target community DID 162 UserDID string // Author DID (for validation) 163 Method string // "social.coves.community.post.create" 164 ExpiresIn int // Token lifetime (seconds) 165} 166 167type ServiceAuthToken struct { 168 Token string // JWT token for auth 169 ExpiresAt time.Time // When token expires 170} 171 172func (c *serviceAuthClient) RequestServiceAuth(ctx context.Context, opts ServiceAuthOptions) (*ServiceAuthToken, error) { 173 endpoint := fmt.Sprintf("%s/xrpc/com.atproto.server.getServiceAuth", opts.RemotePDSURL) 174 175 payload := map[string]interface{}{ 176 "aud": opts.CommunityDID, 177 "exp": time.Now().Add(time.Duration(opts.ExpiresIn) * time.Second).Unix(), 178 "lxm": opts.Method, 179 } 180 181 // Sign request with our instance DID credentials 182 signedReq, err := c.signRequest(payload) 183 if err != nil { 184 return nil, fmt.Errorf("failed to sign service auth request: %w", err) 185 } 186 187 resp, err := c.httpClient.Post(endpoint, signedReq) 188 if err != nil { 189 return nil, fmt.Errorf("service auth request failed: %w", err) 190 } 191 192 return parseServiceAuthResponse(resp) 193} 194``` 195 196### Phase 3: Federated Post Creation 197 198**File:** `internal/core/posts/service.go` 199 200```go 201func (s *postService) createFederatedPost(ctx context.Context, community *communities.Community, req CreatePostRequest) (*CreatePostResponse, error) { 202 // 1. Request service auth token from remote PDS 203 token, err := s.serviceAuthClient.RequestServiceAuth(ctx, service_auth.ServiceAuthOptions{ 204 RemotePDSURL: community.PDSURL, 205 CommunityDID: community.DID, 206 UserDID: req.AuthorDID, 207 Method: "social.coves.community.post.create", 208 ExpiresIn: 300, // 5 minutes 209 }) 210 if err != nil { 211 // Handle specific errors 212 if isUnauthorized(err) { 213 return nil, ErrNotAuthorizedRemote 214 } 215 if isBanned(err) { 216 return nil, ErrBannedRemote 217 } 218 return nil, fmt.Errorf("failed to obtain service auth: %w", err) 219 } 220 221 // 2. Build post record (same as local) 222 postRecord := PostRecord{ 223 Type: "social.coves.community.post", 224 Community: community.DID, 225 Author: req.AuthorDID, 226 Title: req.Title, 227 Content: req.Content, 228 // ... other fields ... 229 CreatedAt: time.Now().UTC().Format(time.RFC3339), 230 } 231 232 // 3. Write to remote PDS using service auth token 233 uri, cid, err := s.createPostOnRemotePDS(ctx, community.PDSURL, community.DID, postRecord, token.Token) 234 if err != nil { 235 return nil, fmt.Errorf("failed to write to remote PDS: %w", err) 236 } 237 238 log.Printf("[FEDERATION] User %s posted to remote community %s: %s", 239 req.AuthorDID, community.DID, uri) 240 241 return &CreatePostResponse{ 242 URI: uri, 243 CID: cid, 244 }, nil 245} 246 247func (s *postService) createPostOnRemotePDS( 248 ctx context.Context, 249 pdsURL string, 250 communityDID string, 251 record PostRecord, 252 serviceAuthToken string, 253) (uri, cid string, err error) { 254 endpoint := fmt.Sprintf("%s/xrpc/com.atproto.repo.createRecord", pdsURL) 255 256 payload := map[string]interface{}{ 257 "repo": communityDID, 258 "collection": "social.coves.community.post", 259 "record": record, 260 } 261 262 jsonData, _ := json.Marshal(payload) 263 req, _ := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(jsonData)) 264 265 // Use service auth token instead of community credentials 266 // NOTE: Auth scheme depends on target PDS implementation: 267 // - Standard atproto service auth uses "Bearer" scheme 268 // - Our AppView uses "DPoP" scheme when DPoP-bound tokens are required 269 // For server-to-server with standard PDS, use Bearer; adjust based on target. 270 req.Header.Set("Authorization", "Bearer "+serviceAuthToken) 271 req.Header.Set("Content-Type", "application/json") 272 273 // ... execute request, parse response ... 274 return uri, cid, nil 275} 276``` 277 278### Phase 4: PDS Service Auth Validation (PDS Extension) 279 280**Note:** This requires extending the PDS. Options: 2811. Contribute to official atproto PDS 2822. Run modified PDS fork 2833. Use PDS middleware/proxy 284 285**Conceptual Implementation:** 286 287```go 288// PDS validates service auth requests before issuing tokens 289func (h *ServiceAuthHandler) HandleGetServiceAuth(w http.ResponseWriter, r *http.Request) { 290 var req ServiceAuthRequest 291 json.NewDecoder(r.Body).Decode(&req) 292 293 // 1. Verify requesting service is trusted 294 requestingDID := extractDIDFromJWT(r.Header.Get("Authorization")) 295 if !h.isTrustedInstance(requestingDID) { 296 writeError(w, http.StatusForbidden, "UntrustedInstance", "Instance not in allowlist") 297 return 298 } 299 300 // 2. Validate community exists on this PDS 301 community, err := h.getCommunityByDID(req.Aud) 302 if err != nil { 303 writeError(w, http.StatusNotFound, "CommunityNotFound", "Community not hosted here") 304 return 305 } 306 307 // 3. Check user not banned (query from AppView or local moderation records) 308 if h.isUserBanned(req.UserDID, req.Aud) { 309 writeError(w, http.StatusForbidden, "Banned", "User banned from community") 310 return 311 } 312 313 // 4. Check community settings (allows remote posts?) 314 if !community.AllowFederatedPosts { 315 writeError(w, http.StatusForbidden, "FederationDisabled", "Community doesn't accept federated posts") 316 return 317 } 318 319 // 5. Rate limiting (per user, per community, per instance) 320 if h.exceedsRateLimit(req.UserDID, req.Aud, requestingDID) { 321 writeError(w, http.StatusTooManyRequests, "RateLimited", "Too many requests") 322 return 323 } 324 325 // 6. Generate scoped token 326 token := h.issueServiceAuthToken(ServiceAuthTokenOptions{ 327 Audience: req.Aud, // Community DID 328 Subject: requestingDID, // Requesting instance DID 329 Method: req.Lxm, // Authorized method 330 ExpiresAt: time.Unix(req.Exp, 0), 331 Scopes: []string{"write:posts"}, 332 }) 333 334 json.NewEncoder(w).Encode(map[string]string{ 335 "token": token, 336 }) 337} 338``` 339 340--- 341 342## Database Schema Changes 343 344### New Table: `instance_federation` 345 346Tracks trusted instances and federation settings: 347 348```sql 349CREATE TABLE instance_federation ( 350 id SERIAL PRIMARY KEY, 351 instance_did TEXT NOT NULL UNIQUE, 352 instance_domain TEXT NOT NULL, 353 trust_level TEXT NOT NULL, -- 'trusted', 'limited', 'blocked' 354 allowed_methods TEXT[] NOT NULL DEFAULT '{}', 355 rate_limit_posts_per_hour INTEGER NOT NULL DEFAULT 100, 356 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 357 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 358 notes TEXT 359); 360 361CREATE INDEX idx_instance_federation_did ON instance_federation(instance_did); 362CREATE INDEX idx_instance_federation_trust ON instance_federation(trust_level); 363``` 364 365### New Table: `federation_rate_limits` 366 367Track federated post rate limits: 368 369```sql 370CREATE TABLE federation_rate_limits ( 371 id SERIAL PRIMARY KEY, 372 user_did TEXT NOT NULL, 373 community_did TEXT NOT NULL, 374 instance_did TEXT NOT NULL, 375 window_start TIMESTAMPTZ NOT NULL, 376 post_count INTEGER NOT NULL DEFAULT 1, 377 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 378 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 379 380 UNIQUE(user_did, community_did, instance_did, window_start) 381); 382 383CREATE INDEX idx_federation_rate_limits_lookup 384 ON federation_rate_limits(user_did, community_did, instance_did, window_start); 385``` 386 387### Update Table: `communities` 388 389Add federation settings: 390 391```sql 392ALTER TABLE communities 393ADD COLUMN allow_federated_posts BOOLEAN NOT NULL DEFAULT true, 394ADD COLUMN federation_mode TEXT NOT NULL DEFAULT 'open'; 395-- federation_mode: 'open' (any instance), 'allowlist' (trusted only), 'local' (no federation) 396``` 397 398--- 399 400## Security Considerations 401 402### 1. Instance Trust Model 403 404**Allowlist Approach (Beta):** 405- Manual approval of federated instances 406- Admin UI to manage instance trust levels 407- Default: block all, explicit allow 408 409**Trust Levels:** 410- `trusted` - Full federation, normal rate limits 411- `limited` - Federation allowed, strict rate limits 412- `blocked` - No federation 413 414### 2. User Ban Synchronization 415 416**Challenge:** Remote instance needs to check local bans 417 418**Options:** 4191. **Service auth validation** - PDS queries AppView for ban status 4202. **Ban records in PDS** - Moderation records stored in community repo 4213. **Cached ban list** - Remote instances cache ban lists (with TTL) 422 423**Beta Approach:** Option 1 (service auth validation queries AppView) 424 425### 3. Rate Limiting 426 427**Multi-level rate limits:** 428- Per user per community: 10 posts/hour 429- Per instance per community: 100 posts/hour 430- Per user across all communities: 50 posts/hour 431 432**Implementation:** In-memory + PostgreSQL for persistence 433 434### 4. Content Validation 435 436**Same validation as local posts:** 437- Lexicon validation 438- Content length limits 439- Embed validation 440- Label validation 441 442**Additional federation checks:** 443- Verify author DID is valid 444- Verify requesting instance signature 445- Verify token scopes match operation 446 447--- 448 449## API Changes 450 451### New Endpoint: `social.coves.federation.getTrustedInstances` 452 453**Purpose:** List instances this instance federates with 454 455**Lexicon:** 456```json 457{ 458 "lexicon": 1, 459 "id": "social.coves.federation.getTrustedInstances", 460 "defs": { 461 "main": { 462 "type": "query", 463 "output": { 464 "encoding": "application/json", 465 "schema": { 466 "type": "object", 467 "required": ["instances"], 468 "properties": { 469 "instances": { 470 "type": "array", 471 "items": { "$ref": "#instanceView" } 472 } 473 } 474 } 475 } 476 }, 477 "instanceView": { 478 "type": "object", 479 "required": ["did", "domain", "trustLevel"], 480 "properties": { 481 "did": { "type": "string" }, 482 "domain": { "type": "string" }, 483 "trustLevel": { "type": "string" }, 484 "allowedMethods": { "type": "array", "items": { "type": "string" } } 485 } 486 } 487 } 488} 489``` 490 491### Modified Endpoint: `social.coves.community.post.create` 492 493**Changes:** 494- No API contract changes 495- Internal routing: local vs federated 496- New error codes: 497 - `FederationFailed` - Remote instance unreachable 498 - `RemoteNotAuthorized` - Remote instance rejected auth 499 - `RemoteBanned` - User banned on remote community 500 501--- 502 503## User Experience 504 505### Happy Path: Cross-Instance Post 506 5071. User on coves.social navigates to !gaming@covesinstance.com 5082. Clicks "Create Post" 5093. Fills out post form (title, content, etc.) 5104. Clicks "Submit" 5115. **Behind the scenes:** 512 - coves.social requests service auth from covesinstance.com 513 - covesinstance.com validates and issues token 514 - coves.social writes post using token 515 - Post appears in feed within seconds (via firehose) 5166. **User sees:** Post published successfully 5177. Post appears in: 518 - covesinstance.com feeds (native community) 519 - coves.social discover/all feeds (indexed via firehose) 520 - User's profile on coves.social 521 522### Error Cases 523 524**User Banned:** 525- Error: "You are banned from !gaming@covesinstance.com" 526- Suggestion: "Contact community moderators for more information" 527 528**Instance Blocked:** 529- Error: "This community does not accept posts from your instance" 530- Suggestion: "Contact community administrators or create a local account" 531 532**Federation Unavailable:** 533- Error: "Unable to connect to covesinstance.com. Try again later." 534- Fallback: Allow saving as draft (future feature) 535 536**Rate Limited:** 537- Error: "You're posting too quickly. Please wait before posting again." 538- Show: Countdown until next post allowed 539 540--- 541 542## Testing Requirements 543 544### Unit Tests 545 5461. **Service Detection:** 547 - `isLocalCommunity()` correctly identifies local vs remote 548 - Handles edge cases (different ports, subdomains) 549 5502. **Service Auth Client:** 551 - Correctly formats service auth requests 552 - Handles token expiration 553 - Retries on transient failures 554 5553. **Federated Post Creation:** 556 - Uses service auth token instead of community credentials 557 - Falls back gracefully on errors 558 - Logs federation events 559 560### Integration Tests 561 5621. **Local Post (Regression):** 563 - Posting to local community still works 564 - No performance degradation 565 5662. **Federated Post:** 567 - User can post to remote community 568 - Service auth token requested correctly 569 - Post written to remote PDS 570 - Post indexed by both AppViews 571 5723. **Authorization Failures:** 573 - Banned users rejected at service auth stage 574 - Untrusted instances rejected 575 - Expired tokens rejected 576 5774. **Rate Limiting:** 578 - Per-user rate limits enforced 579 - Per-instance rate limits enforced 580 - Rate limit resets correctly 581 582### End-to-End Tests 583 5841. **Cross-Instance User Journey:** 585 - Set up two instances (instance-a, instance-b) 586 - Create community on instance-b 587 - User on instance-a posts to instance-b community 588 - Verify post appears on both instances 589 5902. **Moderation Enforcement:** 591 - Ban user on remote instance 592 - Verify user can't post from any instance 593 - Unban user 594 - Verify user can post again 595 5963. **Instance Blocklist:** 597 - Block instance-a on instance-b 598 - Verify users from instance-a can't post to instance-b communities 599 - Unblock instance-a 600 - Verify posting works again 601 602--- 603 604## Migration Path (Alpha → Beta) 605 606### Phase 1: Backend Implementation (No User Impact) 6071. Add service auth client 6082. Add local vs remote detection 6093. Deploy with feature flag `ENABLE_FEDERATION=false` 610 611### Phase 2: Database Migration 6121. Add federation tables 6132. Seed with initial trusted instances (manual) 6143. Add community federation flags (default: allow) 615 616### Phase 3: Soft Launch 6171. Enable federation for single test instance 6182. Monitor service auth requests/errors 6193. Validate rate limiting works 620 621### Phase 4: Beta Rollout 6221. Enable `ENABLE_FEDERATION=true` for all instances 6232. Admin UI for managing trusted instances 6243. Community settings for federation preferences 625 626### Phase 5: Documentation & Onboarding 6271. Instance operator guide: "How to federate with other instances" 6282. Community moderator guide: "Federation settings" 6293. User guide: "Posting across instances" 630 631--- 632 633## Metrics & Success Criteria 634 635### Performance Metrics 636- Service auth request latency: p95 < 200ms 637- Federated post creation time: p95 < 2 seconds (vs 500ms local) 638- Service auth token cache hit rate: > 80% 639 640### Adoption Metrics 641- % of posts that are federated: Target 20% by end of Beta 642- Number of federated instances: Target 5+ by end of Beta 643- Cross-instance engagement (comments, votes): Monitor trend 644 645### Reliability Metrics 646- Service auth success rate: > 99% 647- Federated post success rate: > 95% 648- Service auth token validation errors: < 1% 649 650### Security Metrics 651- Unauthorized access attempts: Monitor & alert 652- Rate limit triggers: Track per instance 653- Ban evasion attempts: Zero tolerance 654 655--- 656 657## Rollback Plan 658 659If federation causes critical issues: 660 6611. **Immediate:** Set `ENABLE_FEDERATION=false` via env var 6622. **Fallback:** All posts route through local-only flow 6633. **Investigation:** Review logs for service auth failures 6644. **Fix Forward:** Deploy patch, re-enable gradually 665 666**No data loss:** Posts are written to PDS, indexed via firehose regardless of federation method. 667 668--- 669 670## Open Questions 671 6721. **Instance Discovery:** How do users find communities on other instances? 673 - Beta: Manual (users share links) 674 - Future: Instance directory, community search across instances 675 6762. **Service Auth Token Caching:** Should AppViews cache service auth tokens? 677 - Pros: Reduce latency, fewer PDS requests 678 - Cons: Stale permissions, ban enforcement delay 679 - **Decision needed:** Cache with short TTL (5 minutes)? 680 6813. **PDS Implementation:** Who implements service auth validation? 682 - Option A: Contribute to official PDS (long timeline) 683 - Option B: Run forked PDS (maintenance burden) 684 - Option C: Proxy/middleware (added complexity) 685 - **Decision needed:** Start with Option B, migrate to Option A? 686 6874. **Federation Symmetry:** If instance-a trusts instance-b, does instance-b auto-trust instance-a? 688 - Beta: No (asymmetric trust) 689 - Future: Mutual federation agreements? 690 6915. **Cross-Instance Moderation:** Should bans propagate across instances? 692 - Beta: No (each instance decides) 693 - Future: Shared moderation lists? 694 695--- 696 697## Future Enhancements (Post-Beta) 698 6991. **Service Auth Token Caching:** Reduce latency for frequent posters 7002. **Batch Service Auth:** Request tokens for multiple communities at once 7013. **Instance Discovery API:** Automatic instance detection/registration 7024. **Federation Analytics:** Dashboard showing cross-instance activity 7035. **Moderation Sync:** Optional shared ban lists across trusted instances 7046. **Content Mirroring:** Cache federated posts locally for performance 7057. **User Migration:** Transfer account between instances 706 707--- 708 709## Resources 710 711### Documentation 712- [atProto Service Auth Spec](https://atproto.com/specs/service-auth) (hypothetical - check actual docs) 713- Lemmy Federation Architecture 714- Mastodon Federation Implementation 715 716### Code References 717- `internal/core/posts/service.go` - Post creation service 718- `internal/api/handlers/post/create.go` - Post creation handler 719- `internal/atproto/jetstream/` - Firehose consumers 720 721### Dependencies 722- atproto SDK (for service auth) 723- PDS v0.4+ (service auth support) 724- PostgreSQL 14+ (for federation tables) 725 726--- 727 728## Appendix A: Service Auth Request Example 729 730**Request to Remote PDS:** 731```http 732POST https://covesinstance.com/xrpc/com.atproto.server.getServiceAuth 733Authorization: DPoP {coves-social-instance-jwt} 734DPoP: {coves-social-dpop-proof} 735Content-Type: application/json 736 737{ 738 "aud": "did:plc:community123", 739 "exp": 1700000000, 740 "lxm": "social.coves.community.post.create" 741} 742``` 743 744**Response:** 745```http 746HTTP/1.1 200 OK 747Content-Type: application/json 748 749{ 750 "token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..." 751} 752``` 753 754**Using Token to Create Post:** 755```http 756POST https://covesinstance.com/xrpc/com.atproto.repo.createRecord 757Authorization: DPoP {service-auth-token} 758DPoP: {service-auth-dpop-proof} 759Content-Type: application/json 760 761{ 762 "repo": "did:plc:community123", 763 "collection": "social.coves.community.post", 764 "record": { 765 "$type": "social.coves.community.post", 766 "community": "did:plc:community123", 767 "author": "did:plc:user456", 768 "title": "Hello from coves.social!", 769 "content": "This is a federated post", 770 "createdAt": "2024-11-16T12:00:00Z" 771 } 772} 773``` 774 775--- 776 777## Appendix B: Error Handling Matrix 778 779| Error Condition | HTTP Status | Error Code | User Message | Retry Strategy | 780|----------------|-------------|------------|--------------|----------------| 781| Instance not trusted | 403 | `UntrustedInstance` | "This community doesn't accept posts from your instance" | No retry | 782| User banned | 403 | `Banned` | "You are banned from this community" | No retry | 783| Rate limit exceeded | 429 | `RateLimited` | "Too many posts. Try again in X minutes" | Exponential backoff | 784| PDS unreachable | 503 | `ServiceUnavailable` | "Community temporarily unavailable" | Retry 3x with backoff | 785| Invalid token | 401 | `InvalidToken` | "Session expired. Please try again" | Refresh token & retry | 786| Community not found | 404 | `CommunityNotFound` | "Community not found" | No retry | 787| Service auth failed | 500 | `FederationFailed` | "Unable to connect. Try again later" | Retry 2x | 788 789--- 790 791**End of PRD**