A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at loom 691 lines 22 kB view raw view rendered
1# Running an ATProto Relay for ATCR Hold Discovery 2 3This document explains what it takes to run an ATProto relay for indexing ATCR hold records, including infrastructure requirements, configuration, and trade-offs. 4 5## Overview 6 7### What is an ATProto Relay? 8 9An ATProto relay is a service that: 10- **Subscribes to multiple PDS hosts** and aggregates their data streams 11- **Outputs a combined "firehose"** event stream for real-time network updates 12- **Validates data integrity** and identity signatures 13- **Provides discovery endpoints** like `com.atproto.sync.listReposByCollection` 14 15The relay acts as a network-wide indexer, making it possible to discover which DIDs have records of specific types (collections). 16 17### Why ATCR Needs a Relay 18 19ATCR uses hold captain records (`io.atcr.hold.captain`) stored in hold PDSs to enable hold discovery. The `listReposByCollection` endpoint allows AppViews to efficiently discover all holds in the network without crawling every PDS individually. 20 21**The problem**: Standard Bluesky relays appear to only index collections from `did:plc` DIDs, not `did:web` DIDs. Since ATCR holds use `did:web` (e.g., `did:web:hold01.atcr.io`), they aren't discoverable via Bluesky's public relays. 22 23## Recommended Approach: Phased Implementation 24 25ATCR's discovery needs evolve as the network grows. Start simple, scale as needed. 26 27## MVP: Minimal Discovery Service 28 29For initial deployment with a small number of holds (dozens, not thousands), build a **lightweight custom discovery service** focused solely on `io.atcr.*` collections. 30 31### Why Minimal Service for MVP? 32 33- **Scope**: Only index `io.atcr.*` collections (manifests, tags, captain/crew, sailor profiles) 34- **Opt-in**: Only crawls PDSs that explicitly call `requestCrawl` 35- **Small scale**: Dozens of holds, not millions of users 36- **Simple storage**: SQLite sufficient for current scale 37- **Cost-effective**: $5-10/month VPS 38 39### Architecture 40 41**Inbound endpoints:** 42``` 43POST /xrpc/com.atproto.sync.requestCrawl 44 → Hold registers itself for crawling 45 46GET /xrpc/com.atproto.sync.listReposByCollection?collection=io.atcr.hold.captain 47 → AppView discovers holds 48``` 49 50**Outbound (client to PDS):** 51``` 521. com.atproto.repo.describeRepo → verify PDS exists 532. com.atproto.sync.getRepo → fetch full CAR file (initial backfill) 543. com.atproto.sync.subscribeRepos → WebSocket for real-time updates 554. Parse events → extract io.atcr.* records → index in SQLite 56``` 57 58**Data flow:** 59 60**Initial crawl (on requestCrawl):** 61``` 621. Hold POSTs requestCrawl → service queues crawl job 632. Service fetches getRepo (CAR file) from hold's PDS for backfill 643. Service parses CAR using indigo libraries 654. Service extracts io.atcr.* records (captain, crew, manifests, etc.) 665. Service stores: (did, collection, rkey, record_data) in SQLite 676. Service opens WebSocket to subscribeRepos for this DID 687. Service stores cursor for reconnection handling 69``` 70 71**Ongoing updates (WebSocket):** 72``` 731. Receive commit events via subscribeRepos WebSocket 742. Parse event, filter to io.atcr.* collections only 753. Update indexed_records incrementally (insert/update/delete) 764. Update cursor after processing each event 775. On disconnect: reconnect with stored cursor to resume 78``` 79 80**Discovery (AppView query):** 81``` 821. AppView GETs listReposByCollection?collection=io.atcr.hold.captain 832. Service queries SQLite WHERE collection='io.atcr.hold.captain' 843. Service returns list of DIDs with that collection 85``` 86 87### Implementation Requirements 88 89**Technologies:** 90- Go (reuse indigo libraries for CAR parsing and WebSocket) 91- SQLite (sufficient for dozens/hundreds of holds) 92- Standard HTTP server + WebSocket client 93 94**Core components:** 95 961. **HTTP handlers** (`cmd/atcr-discovery/handlers/`): 97 - `requestCrawl` - queue crawl jobs 98 - `listReposByCollection` - query indexed collections 99 1002. **Crawler** (`pkg/discovery/crawler.go`): 101 - Fetch CAR files from PDSs for initial backfill 102 - Parse with `github.com/bluesky-social/indigo/repo` 103 - Extract records, filter to `io.atcr.*` only 104 1053. **WebSocket subscriber** (`pkg/discovery/subscriber.go`): 106 - WebSocket client for `com.atproto.sync.subscribeRepos` 107 - Event parsing and filtering 108 - Cursor management and persistence 109 - Automatic reconnection with resume 110 1114. **Storage** (`pkg/discovery/storage.go`): 112 - SQLite schema for indexed records 113 - Indexes on (collection, did) for fast queries 114 - Cursor storage for reconnection 115 1165. **Worker** (`pkg/discovery/worker.go`): 117 - Background crawl job processor 118 - WebSocket connection manager 119 - Health monitoring for subscriptions 120 121**Database schema:** 122```sql 123CREATE TABLE indexed_records ( 124 did TEXT NOT NULL, 125 collection TEXT NOT NULL, 126 rkey TEXT NOT NULL, 127 record_data TEXT NOT NULL, -- JSON 128 indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 129 PRIMARY KEY (did, collection, rkey) 130); 131 132CREATE INDEX idx_collection ON indexed_records(collection); 133CREATE INDEX idx_did ON indexed_records(did); 134 135CREATE TABLE crawl_queue ( 136 id INTEGER PRIMARY KEY AUTOINCREMENT, 137 hostname TEXT NOT NULL UNIQUE, 138 did TEXT, 139 status TEXT DEFAULT 'pending', -- pending, in_progress, subscribed, failed 140 last_crawled_at TIMESTAMP, 141 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 142); 143 144CREATE TABLE subscriptions ( 145 did TEXT PRIMARY KEY, 146 hostname TEXT NOT NULL, 147 cursor INTEGER, -- Last processed sequence number 148 status TEXT DEFAULT 'active', -- active, disconnected, failed 149 last_event_at TIMESTAMP, 150 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 151 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 152); 153``` 154 155**Leveraging indigo libraries:** 156 157```go 158import ( 159 "github.com/bluesky-social/indigo/repo" 160 "github.com/bluesky-social/indigo/atproto/syntax" 161 "github.com/bluesky-social/indigo/events" 162 "github.com/gorilla/websocket" 163 "github.com/ipfs/go-cid" 164) 165 166// Initial backfill: Parse CAR file 167r, err := repo.ReadRepoFromCar(ctx, bytes.NewReader(carData)) 168if err != nil { 169 return err 170} 171 172// Iterate records 173err = r.ForEach(ctx, "", func(path string, nodeCid cid.Cid) error { 174 // Parse collection from path (e.g., "io.atcr.hold.captain/self") 175 parts := strings.Split(path, "/") 176 if len(parts) != 2 { 177 return nil // skip invalid paths 178 } 179 180 collection := parts[0] 181 rkey := parts[1] 182 183 // Filter to io.atcr.* only 184 if !strings.HasPrefix(collection, "io.atcr.") { 185 return nil 186 } 187 188 // Get record data 189 recordBytes, err := r.GetRecord(ctx, path) 190 if err != nil { 191 return err 192 } 193 194 // Store in database 195 return store.IndexRecord(did, collection, rkey, recordBytes) 196}) 197 198// WebSocket subscription: Listen for updates 199wsURL := fmt.Sprintf("wss://%s/xrpc/com.atproto.sync.subscribeRepos", hostname) 200conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) 201if err != nil { 202 return err 203} 204 205// Read events 206rsc := &events.RepoStreamCallbacks{ 207 RepoCommit: func(evt *events.RepoCommit) error { 208 // Filter to io.atcr.* collections only 209 for _, op := range evt.Ops { 210 if !strings.HasPrefix(op.Collection, "io.atcr.") { 211 continue 212 } 213 214 // Process create/update/delete operations 215 switch op.Action { 216 case "create", "update": 217 store.IndexRecord(evt.Repo, op.Collection, op.Rkey, op.Record) 218 case "delete": 219 store.DeleteRecord(evt.Repo, op.Collection, op.Rkey) 220 } 221 } 222 223 // Update cursor 224 return store.UpdateCursor(evt.Repo, evt.Seq) 225 }, 226} 227 228// Process stream 229scheduler := events.NewScheduler("discovery-worker", conn.RemoteAddr().String(), rsc) 230return events.HandleRepoStream(ctx, conn, scheduler) 231``` 232 233### Infrastructure Requirements 234 235**Minimum specs:** 236- 1 vCPU 237- 1-2GB RAM 238- 20GB SSD 239- Minimal bandwidth (<1GB/day for dozens of holds) 240 241**Estimated cost:** 242- Hetzner CX11: €4.15/month (~$5/month) 243- DigitalOcean Basic: $6/month 244- Fly.io: ~$5-10/month 245 246**Deployment:** 247```bash 248# Build 249go build -o atcr-discovery ./cmd/atcr-discovery 250 251# Run 252export DATABASE_PATH="/var/lib/atcr-discovery/discovery.db" 253export HTTP_ADDR=":8080" 254./atcr-discovery 255``` 256 257### Limitations 258 259**What it does NOT do:** 260- ❌ Serve outbound `subscribeRepos` firehose (AppViews query via listReposByCollection) 261- ❌ Full MST validation (trust PDS validation) 262- ❌ Scale to millions of accounts (SQLite limits) 263- ❌ Multi-instance deployment (single process with SQLite) 264 265**When to migrate to full relay:** When you have 1000+ holds, need PostgreSQL, or multi-instance deployment. 266 267## Future Scale: Full Relay (Sync v1.1) 268 269When ATCR grows beyond dozens of holds and needs real-time indexing, migrate to Bluesky's relay v1.1 implementation. 270 271### When to Upgrade 272 273**Indicators:** 274- 100+ holds requesting frequent crawls 275- Need real-time updates (re-crawl latency too high) 276- Multiple AppView instances need coordinated discovery 277- SQLite performance becomes bottleneck 278 279### Relay v1.1 Characteristics 280 281Released May 2025, this is Bluesky's current reference implementation. 282 283**Key features:** 284- **Non-archival**: Doesn't mirror full repository data, only processes firehose 285- **WebSocket subscriptions**: Real-time updates from PDSs 286- **Scalable**: 2 vCPU, 12GB RAM handles ~100M accounts 287- **PostgreSQL**: Required for production scale 288- **Admin UI**: Web dashboard for management 289 290**Source**: `github.com/bluesky-social/indigo/cmd/relay` 291 292### Migration Path 293 294**Step 1: Deploy relay v1.1** 295```bash 296git clone https://github.com/bluesky-social/indigo.git 297cd indigo 298go build -o relay ./cmd/relay 299 300export DATABASE_URL="postgres://relay:password@localhost:5432/atcr_relay" 301./relay --admin-password="secure-password" 302``` 303 304**Step 2: Migrate data** 305- Export indexed records from SQLite 306- Trigger crawls in relay for all known holds 307- Verify relay indexes correctly 308 309**Step 3: Update AppView configuration** 310```bash 311# Point to new relay 312export ATCR_RELAY_ENDPOINT="https://relay.atcr.io" 313``` 314 315**Step 4: Decommission minimal service** 316- Monitor relay for stability 317- Shut down old discovery service 318 319### Infrastructure Requirements (Full Relay) 320 321**Minimum specs:** 322- 2 vCPU cores 323- 12GB RAM 324- 100GB SSD 325- 30 Mbps bandwidth 326 327**Estimated cost:** 328- Hetzner: ~$30-40/month 329- DigitalOcean: ~$50/month (with managed PostgreSQL) 330- Fly.io: ~$35-50/month 331 332## Collection Indexing: The `collectiondir` Microservice 333 334The `com.atproto.sync.listReposByCollection` endpoint is **not part of the relay core**. It's provided by a separate microservice called **`collectiondir`**. 335 336### What is collectiondir? 337 338- **Separate service** that indexes collections for efficient discovery 339- **Optional**: Not required by the ATProto spec, but very useful for AppViews 340- **Deployed alongside relay** by Bluesky's public instances 341 342### Current Limitation: did:plc Only? 343 344Based on testing, Bluesky's public relays (with collectiondir) appear to: 345- ✅ Index `io.atcr.*` collections from `did:plc` DIDs 346- ❌ NOT index `io.atcr.*` collections from `did:web` DIDs 347 348This means: 349- ATCR manifests from users (did:plc) are discoverable 350- ATCR hold captain records (did:web) are NOT discoverable 351- The relay still **stores** all data (CAR file includes did:web records) 352- The issue is specifically with **indexing** for `listReposByCollection` 353 354### Configuring collectiondir 355 356Documentation on configuring collectiondir is sparse. Possible approaches: 357 3581. **Fork and modify**: Clone indigo repo, modify collectiondir to index all DIDs 3592. **Configuration file**: Check if collectiondir accepts whitelist/configuration for indexed collections 3603. **No filtering**: Default behavior might be to index everything, but Bluesky's deployment filters 361 362**Action item**: Review `indigo/cmd/collectiondir` source code to understand configuration options. 363 364## Multi-Relay Strategy 365 366Holds can request crawls from **multiple relays** simultaneously. This enables: 367 368### Scenario: Bluesky + ATCR Relays 369 370**Setup:** 3711. Hold deploys with embedded PDS at `did:web:hold01.atcr.io` 3722. Hold creates captain record (`io.atcr.hold.captain/self`) 3733. Hold requests crawl from **both**: 374 - Bluesky relay: `https://bsky.network/xrpc/com.atproto.sync.requestCrawl` 375 - ATCR relay: `https://relay.atcr.io/xrpc/com.atproto.sync.requestCrawl` 376 377**Result:** 378- ✅ Bluesky relay indexes social posts (if hold owner posts) 379- ✅ ATCR relay indexes hold captain records 380- ✅ AppViews query ATCR relay for hold discovery 381- ✅ Independent networks - Bluesky posts work regardless of ATCR relay 382 383### Request Crawl Script 384 385The existing script can be modified to support multiple relays: 386 387```bash 388#!/bin/bash 389# deploy/request-crawl.sh 390 391HOSTNAME=$1 392BLUESKY_RELAY=${2:-"https://bsky.network"} 393ATCR_RELAY=${3:-"https://relay.atcr.io"} 394 395echo "Requesting crawl for $HOSTNAME from Bluesky relay..." 396curl -X POST "$BLUESKY_RELAY/xrpc/com.atproto.sync.requestCrawl" \ 397 -H "Content-Type: application/json" \ 398 -d "{\"hostname\": \"$HOSTNAME\"}" 399 400echo "Requesting crawl for $HOSTNAME from ATCR relay..." 401curl -X POST "$ATCR_RELAY/xrpc/com.atproto.sync.requestCrawl" \ 402 -H "Content-Type: application/json" \ 403 -d "{\"hostname\": \"$HOSTNAME\"}" 404``` 405 406Usage: 407```bash 408./deploy/request-crawl.sh hold01.atcr.io 409``` 410 411## Deployment: Minimal Discovery Service 412 413### 1. Infrastructure Setup 414 415**Provision VPS:** 416- Hetzner CX11, DigitalOcean Basic, or Fly.io 417- Public domain (e.g., `discovery.atcr.io`) 418- TLS certificate (Let's Encrypt) 419 420**Configure reverse proxy (optional - nginx):** 421```nginx 422upstream discovery { 423 server 127.0.0.1:8080; 424} 425 426server { 427 listen 443 ssl http2; 428 server_name discovery.atcr.io; 429 430 ssl_certificate /etc/letsencrypt/live/discovery.atcr.io/fullchain.pem; 431 ssl_certificate_key /etc/letsencrypt/live/discovery.atcr.io/privkey.pem; 432 433 location / { 434 proxy_pass http://discovery; 435 proxy_set_header Host $host; 436 proxy_set_header X-Real-IP $remote_addr; 437 } 438} 439``` 440 441### 2. Build and Deploy 442 443```bash 444# Clone ATCR repo 445git clone https://github.com/atcr-io/atcr.git 446cd atcr 447 448# Build discovery service 449go build -o atcr-discovery ./cmd/atcr-discovery 450 451# Run 452export DATABASE_PATH="/var/lib/atcr-discovery/discovery.db" 453export HTTP_ADDR=":8080" 454export CRAWL_INTERVAL="12h" 455./atcr-discovery 456``` 457 458### 3. Update Hold Startup 459 460Each hold should request crawl on startup: 461 462```bash 463# In hold startup script or environment 464export ATCR_DISCOVERY_URL="https://discovery.atcr.io" 465 466# Request crawl from both Bluesky and ATCR 467curl -X POST "https://bsky.network/xrpc/com.atproto.sync.requestCrawl" \ 468 -H "Content-Type: application/json" \ 469 -d "{\"hostname\": \"$HOLD_PUBLIC_URL\"}" 470 471curl -X POST "$ATCR_DISCOVERY_URL/xrpc/com.atproto.sync.requestCrawl" \ 472 -H "Content-Type: application/json" \ 473 -d "{\"hostname\": \"$HOLD_PUBLIC_URL\"}" 474``` 475 476### 4. Update AppView Configuration 477 478Point AppView discovery worker to the discovery service: 479 480```bash 481# In .env.appview or environment 482export ATCR_RELAY_ENDPOINT="https://discovery.atcr.io" 483export ATCR_HOLD_DISCOVERY_ENABLED="true" 484export ATCR_HOLD_DISCOVERY_INTERVAL="6h" 485``` 486 487### 5. Monitor and Maintain 488 489**Monitoring:** 490- Check crawl queue status 491- Monitor SQLite database size 492- Track failed crawls 493 494**Maintenance:** 495- Re-crawl on schedule (every 6-24 hours) 496- Prune stale records (>7 days old) 497- Backup SQLite database regularly 498 499## Trade-Offs and Considerations 500 501### Running Your Own Relay 502 503**Pros:** 504- ✅ Full control over indexing (can index `did:web` holds) 505- ✅ No dependency on third-party relay policies 506- ✅ Can customize collection filters for ATCR-specific needs 507- ✅ Relatively lightweight with modern relay implementation 508 509**Cons:** 510- ❌ Infrastructure cost (~$30-50/month minimum) 511- ❌ Operational overhead (monitoring, updates, backups) 512- ❌ Need to maintain as network grows 513- ❌ Single point of failure for discovery (unless multi-relay) 514 515### Alternatives to Running a Relay 516 517#### 1. Direct Registration API 518 519Holds POST to AppView on startup to register themselves: 520 521**Pros:** 522- ✅ Simplest implementation 523- ✅ No relay infrastructure needed 524- ✅ Immediate registration (no crawl delay) 525 526**Cons:** 527- ❌ Ties holds to specific AppView instances 528- ❌ Breaks decentralized discovery model 529- ❌ Each AppView has different hold registry 530 531#### 2. Static Discovery File 532 533Maintain `https://atcr.io/.well-known/holds.json`: 534 535**Pros:** 536- ✅ No infrastructure beyond static hosting 537- ✅ All AppViews share same registry 538- ✅ Simple to implement 539 540**Cons:** 541- ❌ Manual process (PRs/issues to add holds) 542- ❌ Not real-time discovery 543- ❌ Centralized control point 544 545#### 3. Hybrid Approach 546 547Combine multiple discovery mechanisms: 548 549```go 550func (w *HoldDiscoveryWorker) DiscoverHolds(ctx context.Context) error { 551 // 1. Fetch static registry 552 staticHolds := w.fetchStaticRegistry() 553 554 // 2. Query relay (if available) 555 relayHolds := w.queryRelay(ctx) 556 557 // 3. Accept direct registrations 558 registeredHolds := w.getDirectRegistrations() 559 560 // Merge and deduplicate 561 allHolds := mergeHolds(staticHolds, relayHolds, registeredHolds) 562 563 // Cache in database 564 for _, hold := range allHolds { 565 w.cacheHold(hold) 566 } 567} 568``` 569 570**Pros:** 571- ✅ Multiple discovery paths (resilient) 572- ✅ Gradual migration to relay-based discovery 573- ✅ Supports both centralized bootstrap and decentralized growth 574 575**Cons:** 576- ❌ More complex implementation 577- ❌ Potential for stale data if sources conflict 578 579## Recommendations for ATCR 580 581### Phase 1: MVP (Now - 1000 holds) 582 583**Build minimal discovery service with WebSocket** (~$5-10/month): 5841. Implement `requestCrawl` + `listReposByCollection` endpoints 5852. Initial backfill via `getRepo` (CAR file parsing) 5863. Real-time updates via WebSocket `subscribeRepos` 5874. SQLite storage with cursor management 5885. Filter to `io.atcr.*` collections only 589 590**Deliverables:** 591- `cmd/atcr-discovery` service 592- SQLite schema with cursor storage 593- CAR file parser (indigo libraries) 594- WebSocket subscriber with reconnection 595- Deployment scripts 596 597**Cost**: ~$5-10/month VPS 598 599**Why**: Minimal infrastructure, real-time updates, full control over indexing, sufficient for hundreds of holds. 600 601### Phase 2: Migrate to Full Relay (1000+ holds) 602 603**Deploy Bluesky relay v1.1** when scaling needed (~$30-50/month): 6041. Set up PostgreSQL database 6052. Deploy indigo relay with admin UI 6063. Migrate indexed data from SQLite 6074. Configure for `io.atcr.*` collection filtering (if possible) 6085. Handle thousands of concurrent WebSocket connections 609 610**Cost**: ~$30-50/month 611 612**Why**: Proven scalability to 100M+ accounts, standardized protocol, community support, production-ready infrastructure. 613 614### Phase 3: Multi-Relay Federation (Future) 615 616**Decentralized relay network:** 6171. Multiple ATCR relays operated independently 6182. AppViews query multiple relays (fallback/redundancy) 6193. Holds request crawls from all known ATCR relays 6204. Cross-relay synchronization (optional) 621 622**Why**: No single point of failure, fully decentralized discovery, geographic distribution. 623 624## Next Steps 625 626### For MVP Implementation 627 6281. **Create `cmd/atcr-discovery` package structure** 629 - HTTP handlers for XRPC endpoints (`requestCrawl`, `listReposByCollection`) 630 - Crawler with indigo CAR parsing for initial backfill 631 - WebSocket subscriber for real-time updates 632 - SQLite storage layer with cursor management 633 - Background worker for managing subscriptions 634 6352. **Database schema** 636 - `indexed_records` table for collection data 637 - `crawl_queue` table for crawl job management 638 - `subscriptions` table for WebSocket cursor tracking 639 - Indexes for efficient queries 640 6413. **WebSocket implementation** 642 - Use `github.com/bluesky-social/indigo/events` for event handling 643 - Implement reconnection logic with cursor resume 644 - Filter events to `io.atcr.*` collections only 645 - Health monitoring for active subscriptions 646 6474. **Testing strategy** 648 - Unit tests for CAR parsing 649 - Unit tests for event filtering 650 - Integration tests with mock PDSs and WebSocket 651 - Connection failure and reconnection testing 652 - Load testing with SQLite 653 6545. **Deployment** 655 - Dockerfile for discovery service 656 - Deployment scripts (systemd, docker-compose) 657 - Monitoring setup (logs, metrics, WebSocket health) 658 - Alert on subscription failures 659 6606. **Documentation** 661 - API documentation for XRPC endpoints 662 - Deployment guide 663 - Troubleshooting guide (WebSocket connection issues) 664 665### Open Questions 666 6671. **CAR parsing edge cases**: How to handle malformed CAR files or invalid records? 6682. **WebSocket reconnection**: What's the optimal backoff strategy for reconnection attempts? 6693. **Subscription management**: How many concurrent WebSocket connections can SQLite handle? 6704. **Rate limiting**: Should discovery service rate-limit requestCrawl to prevent abuse? 6715. **Authentication**: Should requestCrawl require authentication, or remain open? 6726. **Cursor storage**: Should cursors be persisted immediately or batched for performance? 6737. **Monitoring**: What metrics are most important for operational visibility (active subs, event rate, lag)? 6748. **Error handling**: When a WebSocket dies, should we re-backfill via getRepo or trust cursor resume? 675 676## References 677 678### ATProto Specifications 679- [ATProto Sync Specification](https://atproto.com/specs/sync) 680- [Repository Specification](https://atproto.com/specs/repository) 681- [CAR File Format](https://ipld.io/specs/transport/car/) 682 683### Indigo Libraries 684- [Indigo Repository](https://github.com/bluesky-social/indigo) 685- [Indigo Repo Package](https://pkg.go.dev/github.com/bluesky-social/indigo/repo) 686- [Indigo ATProto Package](https://pkg.go.dev/github.com/bluesky-social/indigo/atproto) 687 688### Relay Reference (Future) 689- [Relay v1.1 Updates](https://docs.bsky.app/blog/relay-sync-updates) 690- [Indigo Relay Implementation](https://github.com/bluesky-social/indigo/tree/main/cmd/relay) 691- [Running a Full-Network Relay](https://whtwnd.com/bnewbold.net/3kwzl7tye6u2y)