A community based topic aggregation platform built on atproto
1# Aggregator Setup Guide
2
3This guide explains how to set up and register an aggregator with Coves instances.
4
5## Table of Contents
6
7- [Overview](#overview)
8- [Architecture](#architecture)
9- [Prerequisites](#prerequisites)
10- [Quick Start](#quick-start)
11- [Detailed Setup Steps](#detailed-setup-steps)
12- [Authorization Process](#authorization-process)
13- [Posting to Communities](#posting-to-communities)
14- [Rate Limits](#rate-limits)
15- [Security Best Practices](#security-best-practices)
16- [Troubleshooting](#troubleshooting)
17- [API Reference](#api-reference)
18
19## Overview
20
21**Aggregators** are automated services that post content to Coves communities. They are similar to Bluesky's feed generators and labelers - self-managed external services that integrate with the platform.
22
23**Key characteristics**:
24- Self-owned: You create and manage your own PDS account
25- Domain-verified: Prove ownership via `.well-known/atproto-did`
26- Community-authorized: Moderators grant posting permission per-community
27- Rate-limited: 10 posts per hour per community
28
29**Example use cases**:
30- RSS feed aggregators (tech news, blog posts)
31- Social media cross-posters (Twitter → Coves)
32- Event notifications (GitHub releases, weather alerts)
33- Content curation bots (daily links, summaries)
34
35## Architecture
36
37### Data Flow
38
39```
40┌──────────────────────────────────────────────────────────┐
41│ 1. One-Time Setup │
42├──────────────────────────────────────────────────────────┤
43│ Aggregator creates PDS account │
44│ ↓ │
45│ Proves domain ownership (.well-known) │
46│ ↓ │
47│ Registers with Coves (enters users table) │
48│ ↓ │
49│ Writes service declaration │
50│ ↓ │
51│ Jetstream indexes into aggregators table │
52└──────────────────────────────────────────────────────────┘
53
54┌──────────────────────────────────────────────────────────┐
55│ 2. Per-Community Authorization │
56├──────────────────────────────────────────────────────────┤
57│ Moderator writes authorization record │
58│ ↓ │
59│ Jetstream indexes into aggregator_authorizations │
60└──────────────────────────────────────────────────────────┘
61
62┌──────────────────────────────────────────────────────────┐
63│ 3. Posting (Ongoing) │
64├──────────────────────────────────────────────────────────┤
65│ Aggregator calls post creation endpoint │
66│ ↓ │
67│ Handler validates: │
68│ - Author in users table ✓ │
69│ - Author in aggregators table ✓ │
70│ - Authorization exists ✓ │
71│ - Rate limit not exceeded ✓ │
72│ ↓ │
73│ Post written to community's PDS │
74│ ↓ │
75│ Jetstream indexes post │
76└──────────────────────────────────────────────────────────┘
77```
78
79### Database Tables
80
81**users** - All actors (users, communities, aggregators)
82```sql
83CREATE TABLE users (
84 did TEXT PRIMARY KEY,
85 handle TEXT NOT NULL,
86 pds_url TEXT,
87 indexed_at TIMESTAMPTZ
88);
89```
90
91**aggregators** - Aggregator-specific metadata
92```sql
93CREATE TABLE aggregators (
94 did TEXT PRIMARY KEY,
95 display_name TEXT NOT NULL,
96 description TEXT,
97 avatar_url TEXT,
98 config_schema JSONB,
99 source_url TEXT,
100 maintainer_did TEXT,
101 record_uri TEXT NOT NULL UNIQUE,
102 record_cid TEXT NOT NULL,
103 created_at TIMESTAMPTZ,
104 indexed_at TIMESTAMPTZ
105);
106```
107
108**aggregator_authorizations** - Community authorizations
109```sql
110CREATE TABLE aggregator_authorizations (
111 id BIGSERIAL PRIMARY KEY,
112 aggregator_did TEXT NOT NULL,
113 community_did TEXT NOT NULL,
114 enabled BOOLEAN NOT NULL DEFAULT true,
115 config JSONB,
116 created_by TEXT,
117 record_uri TEXT NOT NULL UNIQUE,
118 record_cid TEXT NOT NULL,
119 UNIQUE(aggregator_did, community_did)
120);
121```
122
123## Prerequisites
124
1251. **Domain ownership**: You must own a domain where you can host static files over HTTPS
1262. **Web server**: Ability to serve the `.well-known/atproto-did` file
1273. **Development tools**: `curl`, `jq`, basic shell scripting knowledge
1284. **Email address**: For creating the PDS account
129
130**Optional**:
131- Custom avatar image (PNG/JPEG/WebP, max 1MB)
132- GitHub repository for source code transparency
133
134## Quick Start
135
136We provide automated setup scripts:
137
138```bash
139cd scripts/aggregator-setup
140
141# Make scripts executable
142chmod +x *.sh
143
144# Run setup scripts in order
145./1-create-pds-account.sh
146./2-setup-wellknown.sh
147# (Upload .well-known to your web server)
148./3-register-with-coves.sh
149./4-create-service-declaration.sh
150```
151
152See [scripts/aggregator-setup/README.md](../../scripts/aggregator-setup/README.md) for detailed script documentation.
153
154## Detailed Setup Steps
155
156### Step 1: Create PDS Account
157
158Your aggregator needs its own atProto identity (DID). The easiest way is to create an account on an existing PDS.
159
160**Using an existing PDS (recommended)**:
161
162```bash
163curl -X POST https://bsky.social/xrpc/com.atproto.server.createAccount \
164 -H "Content-Type: application/json" \
165 -d '{
166 "handle": "mynewsbot.bsky.social",
167 "email": "bot@example.com",
168 "password": "secure-password-here"
169 }'
170```
171
172**Response**:
173```json
174{
175 "accessJwt": "eyJ...",
176 "refreshJwt": "eyJ...",
177 "handle": "mynewsbot.bsky.social",
178 "did": "did:plc:abc123...",
179 "didDoc": {...}
180}
181```
182
183**Save these credentials securely!** You'll need the DID and access token for all subsequent operations.
184
185**Alternative**: Run your own PDS or use `did:web` (advanced).
186
187### Step 2: Prove Domain Ownership
188
189To register with Coves, you must prove you own a domain by serving your DID at `https://yourdomain.com/.well-known/atproto-did`.
190
191**Create the file**:
192
193```bash
194mkdir -p .well-known
195echo "did:plc:abc123..." > .well-known/atproto-did
196```
197
198**Upload to your web server** so it's accessible at:
199```
200https://rss-bot.example.com/.well-known/atproto-did
201```
202
203**Verify it works**:
204```bash
205curl https://rss-bot.example.com/.well-known/atproto-did
206# Should return: did:plc:abc123...
207```
208
209**Nginx configuration example**:
210```nginx
211location /.well-known/atproto-did {
212 alias /var/www/.well-known/atproto-did;
213 default_type text/plain;
214 add_header Access-Control-Allow-Origin *;
215}
216```
217
218### Step 3: Register with Coves
219
220Call the registration endpoint to register your aggregator DID with the Coves instance.
221
222**Endpoint**: `POST /xrpc/social.coves.aggregator.register`
223
224**Request**:
225```bash
226curl -X POST https://api.coves.social/xrpc/social.coves.aggregator.register \
227 -H "Content-Type: application/json" \
228 -d '{
229 "did": "did:plc:abc123...",
230 "domain": "rss-bot.example.com"
231 }'
232```
233
234**Response** (Success):
235```json
236{
237 "did": "did:plc:abc123...",
238 "handle": "mynewsbot.bsky.social",
239 "message": "Aggregator registered successfully. Next step: create a service declaration record at at://did:plc:abc123.../social.coves.aggregator.service/self"
240}
241```
242
243**What happens**:
2441. Coves fetches `https://rss-bot.example.com/.well-known/atproto-did`
2452. Verifies it contains your DID
2463. Resolves your DID to get handle and PDS URL
2474. Inserts you into the `users` table
248
249**You're now registered!** But you need to create a service declaration next.
250
251### Step 4: Create Service Declaration
252
253Write a `social.coves.aggregator.service` record to your repository. This contains metadata about your aggregator and gets indexed by Coves' Jetstream consumer.
254
255**Endpoint**: `POST https://your-pds.com/xrpc/com.atproto.repo.createRecord`
256
257**Request**:
258```bash
259# Note: This calls the PDS directly, so it uses Bearer authorization (not DPoP)
260curl -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \
261 -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
262 -H "Content-Type: application/json" \
263 -d '{
264 "repo": "did:plc:abc123...",
265 "collection": "social.coves.aggregator.service",
266 "rkey": "self",
267 "record": {
268 "$type": "social.coves.aggregator.service",
269 "did": "did:plc:abc123...",
270 "displayName": "RSS News Aggregator",
271 "description": "Aggregates tech news from various RSS feeds",
272 "sourceUrl": "https://github.com/yourname/rss-aggregator",
273 "maintainer": "did:plc:your-personal-did",
274 "createdAt": "2024-01-15T12:00:00Z"
275 }
276 }'
277```
278
279**Response**:
280```json
281{
282 "uri": "at://did:plc:abc123.../social.coves.aggregator.service/self",
283 "cid": "bafyrei..."
284}
285```
286
287**Optional fields**:
288- `avatar`: Blob reference to avatar image
289- `configSchema`: JSON Schema for community-specific configuration
290
291**Wait 5-10 seconds** for Jetstream to index your service declaration into the `aggregators` table.
292
293## Authorization Process
294
295Before you can post to a community, a moderator must authorize your aggregator.
296
297### How Authorization Works
298
2991. **Moderator decision**: Community moderator evaluates your aggregator
3002. **Authorization record**: Moderator writes `social.coves.aggregator.authorization` to community's repo
3013. **Jetstream indexing**: Record gets indexed into `aggregator_authorizations` table
3024. **Posting enabled**: You can now post to that community
303
304### Authorization Record Structure
305
306**Location**: `at://{community_did}/social.coves.aggregator.authorization/{rkey}`
307
308**Example**:
309```json
310{
311 "$type": "social.coves.aggregator.authorization",
312 "aggregatorDid": "did:plc:abc123...",
313 "communityDid": "did:plc:community123...",
314 "enabled": true,
315 "createdBy": "did:plc:moderator...",
316 "createdAt": "2024-01-15T12:00:00Z",
317 "config": {
318 "maxPostsPerHour": 5,
319 "allowedCategories": ["tech", "news"]
320 }
321}
322```
323
324### Checking Your Authorizations
325
326**Endpoint**: `GET /xrpc/social.coves.aggregator.getAuthorizations`
327
328```bash
329curl "https://api.coves.social/xrpc/social.coves.aggregator.getAuthorizations?aggregatorDid=did:plc:abc123...&enabledOnly=true"
330```
331
332**Response**:
333```json
334{
335 "authorizations": [
336 {
337 "aggregatorDid": "did:plc:abc123...",
338 "communityDid": "did:plc:community123...",
339 "communityHandle": "~tech@coves.social",
340 "enabled": true,
341 "createdAt": "2024-01-15T12:00:00Z",
342 "config": {...}
343 }
344 ]
345}
346```
347
348## Posting to Communities
349
350Once authorized, you can post to communities using the standard post creation endpoint.
351
352### Create Post
353
354**Endpoint**: `POST /xrpc/social.coves.community.post.create`
355
356**Request**:
357```bash
358# Note: This calls the Coves API, so it uses DPoP authorization
359curl -X POST https://api.coves.social/xrpc/social.coves.community.post.create \
360 -H "Authorization: DPoP YOUR_ACCESS_TOKEN" \
361 -H "Content-Type: application/json" \
362 -d '{
363 "communityDid": "did:plc:community123...",
364 "post": {
365 "text": "New blog post: Understanding atProto Identity\nhttps://example.com/post",
366 "createdAt": "2024-01-15T12:00:00Z",
367 "facets": [
368 {
369 "index": { "byteStart": 50, "byteEnd": 75 },
370 "features": [
371 {
372 "$type": "social.coves.richtext.facet#link",
373 "uri": "https://example.com/post"
374 }
375 ]
376 }
377 ]
378 }
379 }'
380```
381
382**Response**:
383```json
384{
385 "uri": "at://did:plc:abc123.../social.coves.community.post/3k...",
386 "cid": "bafyrei..."
387}
388```
389
390### Post Validation
391
392The handler validates:
3931. **Authentication**: Valid JWT token
3942. **Author exists**: DID in `users` table
3953. **Is aggregator**: DID in `aggregators` table
3964. **Authorization**: Active authorization for (aggregator, community)
3975. **Rate limit**: Less than 10 posts/hour to this community
3986. **Content**: Valid post structure per lexicon
399
400### Rate Limits
401
402**Per-community rate limit**: 10 posts per hour
403
404This is tracked in the `aggregator_posts` table and enforced at the handler level.
405
406**Why?**: Prevents spam while allowing useful bot activity.
407
408**Best practices**:
409- Batch similar content
410- Post only high-quality content
411- Respect community guidelines
412- Monitor your posting rate
413
414## Security Best Practices
415
416### Credential Management
417
418✅ **DO**:
419- Store credentials in environment variables or secret management
420- Use HTTPS for all API calls
421- Rotate access tokens regularly (use refresh tokens)
422- Keep `aggregator-config.env` out of version control
423
424❌ **DON'T**:
425- Hardcode credentials in source code
426- Commit credentials to Git
427- Share access tokens publicly
428- Reuse personal credentials for bots
429
430### Domain Security
431
432✅ **DO**:
433- Use HTTPS for `.well-known` endpoint
434- Keep domain under your control
435- Monitor for unauthorized changes
436- Use DNSSEC if possible
437
438❌ **DON'T**:
439- Use HTTP (will fail verification)
440- Use shared/untrusted hosting
441- Allow others to modify `.well-known` files
442- Use expired SSL certificates
443
444### Content Security
445
446✅ **DO**:
447- Validate all external content before posting
448- Sanitize URLs and text
449- Rate-limit your own posting
450- Implement circuit breakers for failures
451
452❌ **DON'T**:
453- Post unvalidated user input
454- Include malicious links
455- Spam communities
456- Bypass rate limits
457
458## Troubleshooting
459
460### Registration Errors
461
462#### Error: "DomainVerificationFailed"
463
464**Cause**: `.well-known/atproto-did` not accessible or contains wrong DID
465
466**Solutions**:
4671. Verify file is accessible: `curl https://yourdomain.com/.well-known/atproto-did`
4682. Check content matches your DID exactly (no extra whitespace)
4693. Ensure HTTPS is working (not HTTP)
4704. Check web server logs for access errors
4715. Verify firewall rules allow HTTPS traffic
472
473#### Error: "AlreadyRegistered"
474
475**Cause**: This DID is already registered with this Coves instance
476
477**Solutions**:
478- This is safe to ignore if you're re-running setup
479- If you need to update info, just create a new service declaration
480- Contact instance admin if you need to remove registration
481
482#### Error: "DIDResolutionFailed"
483
484**Cause**: Could not resolve DID document from PLC directory
485
486**Solutions**:
4871. Verify DID exists: `curl https://plc.directory/{your-did}`
4882. Wait 30 seconds and retry (PLC propagation delay)
4893. Check PDS is accessible
4904. Verify DID format is correct (must start with `did:plc:` or `did:web:`)
491
492### Posting Errors
493
494#### Error: "NotAuthorized"
495
496**Cause**: No active authorization for this (aggregator, community) pair
497
498**Solutions**:
4991. Check authorizations: `GET /xrpc/social.coves.aggregator.getAuthorizations`
5002. Contact community moderator to request authorization
5013. Verify authorization wasn't disabled
5024. Wait for Jetstream to index authorization (5-10 seconds)
503
504#### Error: "RateLimitExceeded"
505
506**Cause**: Exceeded 10 posts/hour to this community
507
508**Solutions**:
5091. Wait for the rate limit window to reset
5102. Batch posts to stay under limit
5113. Distribute posts across multiple communities
5124. Implement posting queue in your aggregator
513
514### Service Declaration Not Appearing
515
516**Symptoms**: Service declaration created but not in `aggregators` table
517
518**Solutions**:
5191. Wait 5-10 seconds for Jetstream to index
5202. Check Jetstream consumer logs for errors
5213. Verify record was created: Check PDS at `at://your-did/social.coves.aggregator.service/self`
5224. Verify `$type` field is exactly `"social.coves.aggregator.service"`
5235. Check `displayName` is not empty (required field)
524
525## API Reference
526
527### Registration Endpoint
528
529**`POST /xrpc/social.coves.aggregator.register`**
530
531**Input**:
532```typescript
533{
534 did: string // DID of aggregator (did:plc or did:web)
535 domain: string // Domain serving .well-known/atproto-did
536}
537```
538
539**Output**:
540```typescript
541{
542 did: string // Registered DID
543 handle: string // Handle from DID document
544 message: string // Next steps message
545}
546```
547
548**Errors**:
549- `InvalidDID`: DID format invalid
550- `DomainVerificationFailed`: .well-known verification failed
551- `AlreadyRegistered`: DID already registered
552- `DIDResolutionFailed`: Could not resolve DID
553
554### Query Endpoints
555
556**`GET /xrpc/social.coves.aggregator.getServices`**
557
558Get aggregator service details.
559
560**Parameters**:
561- `dids`: Array of DIDs (comma-separated)
562
563**`GET /xrpc/social.coves.aggregator.getAuthorizations`**
564
565List communities that authorized an aggregator.
566
567**Parameters**:
568- `aggregatorDid`: Aggregator DID
569- `enabledOnly`: Filter to enabled only (default: false)
570
571**`GET /xrpc/social.coves.aggregator.listForCommunity`**
572
573List aggregators authorized by a community.
574
575**Parameters**:
576- `communityDid`: Community DID
577- `enabledOnly`: Filter to enabled only (default: false)
578
579## Further Reading
580
581- [Aggregator PRD](PRD_AGGREGATORS.md) - Architecture and design decisions
582- [atProto Guide](../../ATPROTO_GUIDE.md) - atProto fundamentals
583- [Communities PRD](../PRD_COMMUNITIES.md) - Community system overview
584- [Setup Scripts README](../../scripts/aggregator-setup/README.md) - Script documentation
585
586## Support
587
588For issues or questions:
589
5901. Check this guide's troubleshooting section
5912. Review the PRD and architecture docs
5923. Check Coves GitHub issues
5934. Ask in Coves developer community