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 req.Header.Set("Authorization", "Bearer "+serviceAuthToken)
267 req.Header.Set("Content-Type", "application/json")
268
269 // ... execute request, parse response ...
270 return uri, cid, nil
271}
272```
273
274### Phase 4: PDS Service Auth Validation (PDS Extension)
275
276**Note:** This requires extending the PDS. Options:
2771. Contribute to official atproto PDS
2782. Run modified PDS fork
2793. Use PDS middleware/proxy
280
281**Conceptual Implementation:**
282
283```go
284// PDS validates service auth requests before issuing tokens
285func (h *ServiceAuthHandler) HandleGetServiceAuth(w http.ResponseWriter, r *http.Request) {
286 var req ServiceAuthRequest
287 json.NewDecoder(r.Body).Decode(&req)
288
289 // 1. Verify requesting service is trusted
290 requestingDID := extractDIDFromJWT(r.Header.Get("Authorization"))
291 if !h.isTrustedInstance(requestingDID) {
292 writeError(w, http.StatusForbidden, "UntrustedInstance", "Instance not in allowlist")
293 return
294 }
295
296 // 2. Validate community exists on this PDS
297 community, err := h.getCommunityByDID(req.Aud)
298 if err != nil {
299 writeError(w, http.StatusNotFound, "CommunityNotFound", "Community not hosted here")
300 return
301 }
302
303 // 3. Check user not banned (query from AppView or local moderation records)
304 if h.isUserBanned(req.UserDID, req.Aud) {
305 writeError(w, http.StatusForbidden, "Banned", "User banned from community")
306 return
307 }
308
309 // 4. Check community settings (allows remote posts?)
310 if !community.AllowFederatedPosts {
311 writeError(w, http.StatusForbidden, "FederationDisabled", "Community doesn't accept federated posts")
312 return
313 }
314
315 // 5. Rate limiting (per user, per community, per instance)
316 if h.exceedsRateLimit(req.UserDID, req.Aud, requestingDID) {
317 writeError(w, http.StatusTooManyRequests, "RateLimited", "Too many requests")
318 return
319 }
320
321 // 6. Generate scoped token
322 token := h.issueServiceAuthToken(ServiceAuthTokenOptions{
323 Audience: req.Aud, // Community DID
324 Subject: requestingDID, // Requesting instance DID
325 Method: req.Lxm, // Authorized method
326 ExpiresAt: time.Unix(req.Exp, 0),
327 Scopes: []string{"write:posts"},
328 })
329
330 json.NewEncoder(w).Encode(map[string]string{
331 "token": token,
332 })
333}
334```
335
336---
337
338## Database Schema Changes
339
340### New Table: `instance_federation`
341
342Tracks trusted instances and federation settings:
343
344```sql
345CREATE TABLE instance_federation (
346 id SERIAL PRIMARY KEY,
347 instance_did TEXT NOT NULL UNIQUE,
348 instance_domain TEXT NOT NULL,
349 trust_level TEXT NOT NULL, -- 'trusted', 'limited', 'blocked'
350 allowed_methods TEXT[] NOT NULL DEFAULT '{}',
351 rate_limit_posts_per_hour INTEGER NOT NULL DEFAULT 100,
352 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
353 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
354 notes TEXT
355);
356
357CREATE INDEX idx_instance_federation_did ON instance_federation(instance_did);
358CREATE INDEX idx_instance_federation_trust ON instance_federation(trust_level);
359```
360
361### New Table: `federation_rate_limits`
362
363Track federated post rate limits:
364
365```sql
366CREATE TABLE federation_rate_limits (
367 id SERIAL PRIMARY KEY,
368 user_did TEXT NOT NULL,
369 community_did TEXT NOT NULL,
370 instance_did TEXT NOT NULL,
371 window_start TIMESTAMPTZ NOT NULL,
372 post_count INTEGER NOT NULL DEFAULT 1,
373 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
374 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
375
376 UNIQUE(user_did, community_did, instance_did, window_start)
377);
378
379CREATE INDEX idx_federation_rate_limits_lookup
380 ON federation_rate_limits(user_did, community_did, instance_did, window_start);
381```
382
383### Update Table: `communities`
384
385Add federation settings:
386
387```sql
388ALTER TABLE communities
389ADD COLUMN allow_federated_posts BOOLEAN NOT NULL DEFAULT true,
390ADD COLUMN federation_mode TEXT NOT NULL DEFAULT 'open';
391-- federation_mode: 'open' (any instance), 'allowlist' (trusted only), 'local' (no federation)
392```
393
394---
395
396## Security Considerations
397
398### 1. Instance Trust Model
399
400**Allowlist Approach (Beta):**
401- Manual approval of federated instances
402- Admin UI to manage instance trust levels
403- Default: block all, explicit allow
404
405**Trust Levels:**
406- `trusted` - Full federation, normal rate limits
407- `limited` - Federation allowed, strict rate limits
408- `blocked` - No federation
409
410### 2. User Ban Synchronization
411
412**Challenge:** Remote instance needs to check local bans
413
414**Options:**
4151. **Service auth validation** - PDS queries AppView for ban status
4162. **Ban records in PDS** - Moderation records stored in community repo
4173. **Cached ban list** - Remote instances cache ban lists (with TTL)
418
419**Beta Approach:** Option 1 (service auth validation queries AppView)
420
421### 3. Rate Limiting
422
423**Multi-level rate limits:**
424- Per user per community: 10 posts/hour
425- Per instance per community: 100 posts/hour
426- Per user across all communities: 50 posts/hour
427
428**Implementation:** In-memory + PostgreSQL for persistence
429
430### 4. Content Validation
431
432**Same validation as local posts:**
433- Lexicon validation
434- Content length limits
435- Embed validation
436- Label validation
437
438**Additional federation checks:**
439- Verify author DID is valid
440- Verify requesting instance signature
441- Verify token scopes match operation
442
443---
444
445## API Changes
446
447### New Endpoint: `social.coves.federation.getTrustedInstances`
448
449**Purpose:** List instances this instance federates with
450
451**Lexicon:**
452```json
453{
454 "lexicon": 1,
455 "id": "social.coves.federation.getTrustedInstances",
456 "defs": {
457 "main": {
458 "type": "query",
459 "output": {
460 "encoding": "application/json",
461 "schema": {
462 "type": "object",
463 "required": ["instances"],
464 "properties": {
465 "instances": {
466 "type": "array",
467 "items": { "$ref": "#instanceView" }
468 }
469 }
470 }
471 }
472 },
473 "instanceView": {
474 "type": "object",
475 "required": ["did", "domain", "trustLevel"],
476 "properties": {
477 "did": { "type": "string" },
478 "domain": { "type": "string" },
479 "trustLevel": { "type": "string" },
480 "allowedMethods": { "type": "array", "items": { "type": "string" } }
481 }
482 }
483 }
484}
485```
486
487### Modified Endpoint: `social.coves.community.post.create`
488
489**Changes:**
490- No API contract changes
491- Internal routing: local vs federated
492- New error codes:
493 - `FederationFailed` - Remote instance unreachable
494 - `RemoteNotAuthorized` - Remote instance rejected auth
495 - `RemoteBanned` - User banned on remote community
496
497---
498
499## User Experience
500
501### Happy Path: Cross-Instance Post
502
5031. User on coves.social navigates to !gaming@covesinstance.com
5042. Clicks "Create Post"
5053. Fills out post form (title, content, etc.)
5064. Clicks "Submit"
5075. **Behind the scenes:**
508 - coves.social requests service auth from covesinstance.com
509 - covesinstance.com validates and issues token
510 - coves.social writes post using token
511 - Post appears in feed within seconds (via firehose)
5126. **User sees:** Post published successfully
5137. Post appears in:
514 - covesinstance.com feeds (native community)
515 - coves.social discover/all feeds (indexed via firehose)
516 - User's profile on coves.social
517
518### Error Cases
519
520**User Banned:**
521- Error: "You are banned from !gaming@covesinstance.com"
522- Suggestion: "Contact community moderators for more information"
523
524**Instance Blocked:**
525- Error: "This community does not accept posts from your instance"
526- Suggestion: "Contact community administrators or create a local account"
527
528**Federation Unavailable:**
529- Error: "Unable to connect to covesinstance.com. Try again later."
530- Fallback: Allow saving as draft (future feature)
531
532**Rate Limited:**
533- Error: "You're posting too quickly. Please wait before posting again."
534- Show: Countdown until next post allowed
535
536---
537
538## Testing Requirements
539
540### Unit Tests
541
5421. **Service Detection:**
543 - `isLocalCommunity()` correctly identifies local vs remote
544 - Handles edge cases (different ports, subdomains)
545
5462. **Service Auth Client:**
547 - Correctly formats service auth requests
548 - Handles token expiration
549 - Retries on transient failures
550
5513. **Federated Post Creation:**
552 - Uses service auth token instead of community credentials
553 - Falls back gracefully on errors
554 - Logs federation events
555
556### Integration Tests
557
5581. **Local Post (Regression):**
559 - Posting to local community still works
560 - No performance degradation
561
5622. **Federated Post:**
563 - User can post to remote community
564 - Service auth token requested correctly
565 - Post written to remote PDS
566 - Post indexed by both AppViews
567
5683. **Authorization Failures:**
569 - Banned users rejected at service auth stage
570 - Untrusted instances rejected
571 - Expired tokens rejected
572
5734. **Rate Limiting:**
574 - Per-user rate limits enforced
575 - Per-instance rate limits enforced
576 - Rate limit resets correctly
577
578### End-to-End Tests
579
5801. **Cross-Instance User Journey:**
581 - Set up two instances (instance-a, instance-b)
582 - Create community on instance-b
583 - User on instance-a posts to instance-b community
584 - Verify post appears on both instances
585
5862. **Moderation Enforcement:**
587 - Ban user on remote instance
588 - Verify user can't post from any instance
589 - Unban user
590 - Verify user can post again
591
5923. **Instance Blocklist:**
593 - Block instance-a on instance-b
594 - Verify users from instance-a can't post to instance-b communities
595 - Unblock instance-a
596 - Verify posting works again
597
598---
599
600## Migration Path (Alpha → Beta)
601
602### Phase 1: Backend Implementation (No User Impact)
6031. Add service auth client
6042. Add local vs remote detection
6053. Deploy with feature flag `ENABLE_FEDERATION=false`
606
607### Phase 2: Database Migration
6081. Add federation tables
6092. Seed with initial trusted instances (manual)
6103. Add community federation flags (default: allow)
611
612### Phase 3: Soft Launch
6131. Enable federation for single test instance
6142. Monitor service auth requests/errors
6153. Validate rate limiting works
616
617### Phase 4: Beta Rollout
6181. Enable `ENABLE_FEDERATION=true` for all instances
6192. Admin UI for managing trusted instances
6203. Community settings for federation preferences
621
622### Phase 5: Documentation & Onboarding
6231. Instance operator guide: "How to federate with other instances"
6242. Community moderator guide: "Federation settings"
6253. User guide: "Posting across instances"
626
627---
628
629## Metrics & Success Criteria
630
631### Performance Metrics
632- Service auth request latency: p95 < 200ms
633- Federated post creation time: p95 < 2 seconds (vs 500ms local)
634- Service auth token cache hit rate: > 80%
635
636### Adoption Metrics
637- % of posts that are federated: Target 20% by end of Beta
638- Number of federated instances: Target 5+ by end of Beta
639- Cross-instance engagement (comments, votes): Monitor trend
640
641### Reliability Metrics
642- Service auth success rate: > 99%
643- Federated post success rate: > 95%
644- Service auth token validation errors: < 1%
645
646### Security Metrics
647- Unauthorized access attempts: Monitor & alert
648- Rate limit triggers: Track per instance
649- Ban evasion attempts: Zero tolerance
650
651---
652
653## Rollback Plan
654
655If federation causes critical issues:
656
6571. **Immediate:** Set `ENABLE_FEDERATION=false` via env var
6582. **Fallback:** All posts route through local-only flow
6593. **Investigation:** Review logs for service auth failures
6604. **Fix Forward:** Deploy patch, re-enable gradually
661
662**No data loss:** Posts are written to PDS, indexed via firehose regardless of federation method.
663
664---
665
666## Open Questions
667
6681. **Instance Discovery:** How do users find communities on other instances?
669 - Beta: Manual (users share links)
670 - Future: Instance directory, community search across instances
671
6722. **Service Auth Token Caching:** Should AppViews cache service auth tokens?
673 - Pros: Reduce latency, fewer PDS requests
674 - Cons: Stale permissions, ban enforcement delay
675 - **Decision needed:** Cache with short TTL (5 minutes)?
676
6773. **PDS Implementation:** Who implements service auth validation?
678 - Option A: Contribute to official PDS (long timeline)
679 - Option B: Run forked PDS (maintenance burden)
680 - Option C: Proxy/middleware (added complexity)
681 - **Decision needed:** Start with Option B, migrate to Option A?
682
6834. **Federation Symmetry:** If instance-a trusts instance-b, does instance-b auto-trust instance-a?
684 - Beta: No (asymmetric trust)
685 - Future: Mutual federation agreements?
686
6875. **Cross-Instance Moderation:** Should bans propagate across instances?
688 - Beta: No (each instance decides)
689 - Future: Shared moderation lists?
690
691---
692
693## Future Enhancements (Post-Beta)
694
6951. **Service Auth Token Caching:** Reduce latency for frequent posters
6962. **Batch Service Auth:** Request tokens for multiple communities at once
6973. **Instance Discovery API:** Automatic instance detection/registration
6984. **Federation Analytics:** Dashboard showing cross-instance activity
6995. **Moderation Sync:** Optional shared ban lists across trusted instances
7006. **Content Mirroring:** Cache federated posts locally for performance
7017. **User Migration:** Transfer account between instances
702
703---
704
705## Resources
706
707### Documentation
708- [atProto Service Auth Spec](https://atproto.com/specs/service-auth) (hypothetical - check actual docs)
709- Lemmy Federation Architecture
710- Mastodon Federation Implementation
711
712### Code References
713- `internal/core/posts/service.go` - Post creation service
714- `internal/api/handlers/post/create.go` - Post creation handler
715- `internal/atproto/jetstream/` - Firehose consumers
716
717### Dependencies
718- atproto SDK (for service auth)
719- PDS v0.4+ (service auth support)
720- PostgreSQL 14+ (for federation tables)
721
722---
723
724## Appendix A: Service Auth Request Example
725
726**Request to Remote PDS:**
727```http
728POST https://covesinstance.com/xrpc/com.atproto.server.getServiceAuth
729Authorization: Bearer {coves-social-instance-jwt}
730Content-Type: application/json
731
732{
733 "aud": "did:plc:community123",
734 "exp": 1700000000,
735 "lxm": "social.coves.community.post.create"
736}
737```
738
739**Response:**
740```http
741HTTP/1.1 200 OK
742Content-Type: application/json
743
744{
745 "token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
746}
747```
748
749**Using Token to Create Post:**
750```http
751POST https://covesinstance.com/xrpc/com.atproto.repo.createRecord
752Authorization: Bearer {service-auth-token}
753Content-Type: application/json
754
755{
756 "repo": "did:plc:community123",
757 "collection": "social.coves.community.post",
758 "record": {
759 "$type": "social.coves.community.post",
760 "community": "did:plc:community123",
761 "author": "did:plc:user456",
762 "title": "Hello from coves.social!",
763 "content": "This is a federated post",
764 "createdAt": "2024-11-16T12:00:00Z"
765 }
766}
767```
768
769---
770
771## Appendix B: Error Handling Matrix
772
773| Error Condition | HTTP Status | Error Code | User Message | Retry Strategy |
774|----------------|-------------|------------|--------------|----------------|
775| Instance not trusted | 403 | `UntrustedInstance` | "This community doesn't accept posts from your instance" | No retry |
776| User banned | 403 | `Banned` | "You are banned from this community" | No retry |
777| Rate limit exceeded | 429 | `RateLimited` | "Too many posts. Try again in X minutes" | Exponential backoff |
778| PDS unreachable | 503 | `ServiceUnavailable` | "Community temporarily unavailable" | Retry 3x with backoff |
779| Invalid token | 401 | `InvalidToken` | "Session expired. Please try again" | Refresh token & retry |
780| Community not found | 404 | `CommunityNotFound` | "Community not found" | No retry |
781| Service auth failed | 500 | `FederationFailed` | "Unable to connect. Try again later" | Retry 2x |
782
783---
784
785**End of PRD**