A community based topic aggregation platform built on atproto

docs: add Federation PRD for Beta cross-instance posting

Added comprehensive PRD for implementing Lemmy-style federation in Beta:

**Overview:**
- Enable users on any Coves instance to post to communities on other instances
- Maintain community ownership (posts live in community repos)
- Use atProto-native service authentication pattern

**Key Features:**
- Cross-instance posting: user@instance-a posts to !community@instance-b
- atProto service auth via com.atproto.server.getServiceAuth
- Community moderation control maintained
- Allowlist-based federation (manual for Beta)

**Technical Approach:**
- Service-to-service JWT authentication
- Community credentials delegation to user's instance
- Proper record ownership in community repos
- Security: signature verification, rate limiting, moderation

**Deferred to Future:**
- Automatic instance discovery (Beta uses manual allowlist)
- Cross-instance moderation delegation
- Content mirroring/replication
- User migration between instances

**Target:** Beta Release

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

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

+785
+785
docs/federation-prd.md
··· 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 + 12 + Enable 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 32 + 1. **Enable cross-instance posting** - Users can post to any community on any federated instance 33 + 2. **Preserve community ownership** - Posts live in community repos, not user repos 34 + 3. **atProto-native implementation** - Use `com.atproto.server.getServiceAuth` pattern 35 + 4. **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 + 49 + Use 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 127 + func (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 + 142 + func (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 154 + type ServiceAuthClient interface { 155 + // RequestServiceAuth obtains a scoped token for writing to remote community 156 + RequestServiceAuth(ctx context.Context, opts ServiceAuthOptions) (*ServiceAuthToken, error) 157 + } 158 + 159 + type 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 + 167 + type ServiceAuthToken struct { 168 + Token string // JWT token for auth 169 + ExpiresAt time.Time // When token expires 170 + } 171 + 172 + func (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 201 + func (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 + 247 + func (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: 277 + 1. Contribute to official atproto PDS 278 + 2. Run modified PDS fork 279 + 3. Use PDS middleware/proxy 280 + 281 + **Conceptual Implementation:** 282 + 283 + ```go 284 + // PDS validates service auth requests before issuing tokens 285 + func (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 + 342 + Tracks trusted instances and federation settings: 343 + 344 + ```sql 345 + CREATE 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 + 357 + CREATE INDEX idx_instance_federation_did ON instance_federation(instance_did); 358 + CREATE INDEX idx_instance_federation_trust ON instance_federation(trust_level); 359 + ``` 360 + 361 + ### New Table: `federation_rate_limits` 362 + 363 + Track federated post rate limits: 364 + 365 + ```sql 366 + CREATE 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 + 379 + CREATE 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 + 385 + Add federation settings: 386 + 387 + ```sql 388 + ALTER TABLE communities 389 + ADD COLUMN allow_federated_posts BOOLEAN NOT NULL DEFAULT true, 390 + ADD 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:** 415 + 1. **Service auth validation** - PDS queries AppView for ban status 416 + 2. **Ban records in PDS** - Moderation records stored in community repo 417 + 3. **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 + 503 + 1. User on coves.social navigates to !gaming@covesinstance.com 504 + 2. Clicks "Create Post" 505 + 3. Fills out post form (title, content, etc.) 506 + 4. Clicks "Submit" 507 + 5. **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) 512 + 6. **User sees:** Post published successfully 513 + 7. 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 + 542 + 1. **Service Detection:** 543 + - `isLocalCommunity()` correctly identifies local vs remote 544 + - Handles edge cases (different ports, subdomains) 545 + 546 + 2. **Service Auth Client:** 547 + - Correctly formats service auth requests 548 + - Handles token expiration 549 + - Retries on transient failures 550 + 551 + 3. **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 + 558 + 1. **Local Post (Regression):** 559 + - Posting to local community still works 560 + - No performance degradation 561 + 562 + 2. **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 + 568 + 3. **Authorization Failures:** 569 + - Banned users rejected at service auth stage 570 + - Untrusted instances rejected 571 + - Expired tokens rejected 572 + 573 + 4. **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 + 580 + 1. **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 + 586 + 2. **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 + 592 + 3. **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) 603 + 1. Add service auth client 604 + 2. Add local vs remote detection 605 + 3. Deploy with feature flag `ENABLE_FEDERATION=false` 606 + 607 + ### Phase 2: Database Migration 608 + 1. Add federation tables 609 + 2. Seed with initial trusted instances (manual) 610 + 3. Add community federation flags (default: allow) 611 + 612 + ### Phase 3: Soft Launch 613 + 1. Enable federation for single test instance 614 + 2. Monitor service auth requests/errors 615 + 3. Validate rate limiting works 616 + 617 + ### Phase 4: Beta Rollout 618 + 1. Enable `ENABLE_FEDERATION=true` for all instances 619 + 2. Admin UI for managing trusted instances 620 + 3. Community settings for federation preferences 621 + 622 + ### Phase 5: Documentation & Onboarding 623 + 1. Instance operator guide: "How to federate with other instances" 624 + 2. Community moderator guide: "Federation settings" 625 + 3. 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 + 655 + If federation causes critical issues: 656 + 657 + 1. **Immediate:** Set `ENABLE_FEDERATION=false` via env var 658 + 2. **Fallback:** All posts route through local-only flow 659 + 3. **Investigation:** Review logs for service auth failures 660 + 4. **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 + 668 + 1. **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 + 672 + 2. **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 + 677 + 3. **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 + 683 + 4. **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 + 687 + 5. **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 + 695 + 1. **Service Auth Token Caching:** Reduce latency for frequent posters 696 + 2. **Batch Service Auth:** Request tokens for multiple communities at once 697 + 3. **Instance Discovery API:** Automatic instance detection/registration 698 + 4. **Federation Analytics:** Dashboard showing cross-instance activity 699 + 5. **Moderation Sync:** Optional shared ban lists across trusted instances 700 + 6. **Content Mirroring:** Cache federated posts locally for performance 701 + 7. **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 728 + POST https://covesinstance.com/xrpc/com.atproto.server.getServiceAuth 729 + Authorization: Bearer {coves-social-instance-jwt} 730 + Content-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 741 + HTTP/1.1 200 OK 742 + Content-Type: application/json 743 + 744 + { 745 + "token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..." 746 + } 747 + ``` 748 + 749 + **Using Token to Create Post:** 750 + ```http 751 + POST https://covesinstance.com/xrpc/com.atproto.repo.createRecord 752 + Authorization: Bearer {service-auth-token} 753 + Content-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**