A community based topic aggregation platform built on atproto

refactor(community): remove handle and stats from community profile records

Following atProto best practices, community profile records now only contain
user-controlled data. Handles are mutable and resolved from DIDs via PLC, so
they should not be stored in immutable records. Member/subscriber counts are
AppView-computed stats, not record data.

Changes:
- Remove 'handle' field from community profile record creation
- Remove 'handle' field from community profile record updates
- Remove 'memberCount' and 'subscriberCount' from profile records
- Update E2E test to not expect handle in PDS record
- Update consumer test mock data to match new record schema

AppView caching (Go structs) still maintains these fields for performance:
- service.go:190 - Community struct keeps Handle field
- community_consumer.go:159,241 - Consumer reads handle for caching

This matches Bluesky's app.bsky.actor.profile pattern where handles are
resolved from DIDs, not stored in profile records.

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

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

Changed files
+11 -28
internal
core
communities
tests
+1 -11
internal/core/communities/service.go
···
// Build community profile record
profile := map[string]interface{}{
"$type": "social.coves.community.profile",
-
"handle": pdsAccount.Handle, // atProto handle (e.g., gaming.community.coves.social)
-
"name": req.Name, // Short name for !mentions (e.g., "gaming")
+
"name": req.Name, // Short name for !mentions (e.g., "gaming")
"visibility": req.Visibility,
"hostedBy": s.instanceDID, // V2: Instance hosts, community owns
"createdBy": req.CreatedByDID,
···
if req.Language != "" {
profile["language"] = req.Language
}
-
-
// Initialize counts
-
profile["memberCount"] = 0
-
profile["subscriberCount"] = 0
// TODO: Handle avatar and banner blobs
// For now, we'll skip blob uploads. This would require:
···
// Build updated profile record (start with existing)
profile := map[string]interface{}{
"$type": "social.coves.community.profile",
-
"handle": existing.Handle,
"name": existing.Name,
"owner": existing.OwnerDID,
"createdBy": existing.CreatedByDID,
···
} else if len(existing.ContentWarnings) > 0 {
profile["contentWarnings"] = existing.ContentWarnings
}
-
-
// Preserve counts
-
profile["memberCount"] = existing.MemberCount
-
profile["subscriberCount"] = existing.SubscriberCount
// V2: Community profiles always use "self" as rkey
// (No need to extract from URI - it's always "self" for V2 communities)
+6 -10
tests/integration/community_consumer_test.go
···
RKey: "self",
CID: "bafy123abc",
Record: map[string]interface{}{
-
"did": communityDID, // Community's unique DID
-
"handle": fmt.Sprintf("!test-community-%s@coves.local", uniqueSuffix),
+
// Note: No 'did', 'handle', 'memberCount', or 'subscriberCount' in record
+
// These are resolved/computed by AppView, not stored in immutable records
"name": "test-community",
"displayName": "Test Community",
"description": "A test community",
···
"federation": map[string]interface{}{
"allowExternalDiscovery": true,
},
-
"memberCount": 0,
-
"subscriberCount": 0,
-
"createdAt": time.Now().Format(time.RFC3339),
+
"createdAt": time.Now().Format(time.RFC3339),
},
},
}
···
RKey: "self",
CID: "bafy456def",
Record: map[string]interface{}{
-
"did": communityDID, // Community's unique DID
-
"handle": handle,
+
// Note: No 'did', 'handle', 'memberCount', or 'subscriberCount' in record
+
// These are resolved/computed by AppView, not stored in immutable records
"name": "update-test",
"displayName": "Updated Name",
"description": "Updated description",
···
"federation": map[string]interface{}{
"allowExternalDiscovery": false,
},
-
"memberCount": 5,
-
"subscriberCount": 10,
-
"createdAt": time.Now().Format(time.RFC3339),
+
"createdAt": time.Now().Format(time.RFC3339),
},
},
}
+4 -7
tests/integration/community_e2e_test.go
···
t.Logf(" Record value:\n %s", string(recordJSON))
}
-
// V2: DID is NOT in the record - it's in the repository URI
-
// The record should have handle, name, etc. but no 'did' field
-
// This matches Bluesky's app.bsky.actor.profile pattern
-
if pdsRecord.Value["handle"] != community.Handle {
-
t.Errorf("Community handle mismatch in PDS record: expected %s, got %v",
-
community.Handle, pdsRecord.Value["handle"])
-
}
+
// V2: DID and Handle are NOT in the record - they're resolved from the repository URI
+
// The record should have name, hostedBy, createdBy, etc. but no 'did' or 'handle' fields
+
// This matches Bluesky's app.bsky.actor.profile pattern (no handle in record)
+
// Handles are mutable and resolved from DIDs via PLC, so they shouldn't be stored in immutable records
// ====================================================================================
// Part 2: TRUE E2E - Real Jetstream Firehose Consumer