Community Manager Pattern: Forums

In my last post , I talked about the community manager pattern. I focused on the basics including how membership attestations work, how the wrapper pattern handles content, and what the lifecycle of a book club community looks like.

This time, I want to show a use case that isn't possible yet, but would fit well with the community manager pattern: discussion forums.

The Problem Today

Forums have been around since the early days of the internet, but there's always been a basic conflict: users want to own their posts, while communities need to moderate content. Traditional forums solve this by owning everything. Your posts live on their servers and follow their rules. ATProtocol's data ownership model changes the picture.

If you wanted to build a forum on ATProtocol right now, you'd face an awkward choice. Either users post content to their own repositories (preserving ownership but making moderation nearly impossible), or some central identity owns all the content (enabling moderation but abandoning the protocol’s core value proposition).

Neither of these options is ideal. The community manager pattern offers a better solution.

How It Would Work

Imagine a local music forum at dayton-music-scene.com. The forum itself is an ATProtocol identity with its own DID and repository. It has a community manager service that handles membership and content.

Membership is simple but still meaningful. Most forums want easy signups, unlike a book club that may carefully curates members. The community manager can set up automatic approval, so anyone who creates a membership record is attested right away, maybe with basic checks like account age or email verification. The attestation remains, so the community can revoke it later if needed.

Topics, Threads, and Wrappers

Let's look at how content moves through this model. There are three main types of records involved.

Topics belong to the community.

A topic is a category or channel for discussion, like "General", "Show Announcements", or "Gear Talk". These are stored in the community's repository because they organize the forum at the community level:

{
"uri": "at://did:plc:dayton-music-scene/community.lexicon.discussion.topic/1",
"cid": "bafyreib7phelijh23iktyzraqw54wi7flwthmdzryxscmtvb47cggcfey4",
"value": {
"$type": "community.lexicon.discussion.topic",
"name": "General"
}
}

Threads are owned by users.

When I want to start a discussion, I write the thread content to my own PDS. This is my data. I'm the author, and I control it:

{
"uri": "at://did:plc:nick/community.lexicon.discussion.thread/1",
"cid": "bafyreihg2rtnzae4wtclusodra5pe4luscjtt66ltwsgwq2zy4jhjhhg44",
"value": {
"$type": "community.lexicon.discussion.thread",
"text": "Hey there bud, how'd it go last night?\nI saw you at the band stand looking pretty slammed. https://www.peachpitmusic.com/",
"facets": [
{
"index": {
"byteStart": 92,
"byteEnd": 122
},
"features": [
{
"$type": "app.bsky.richtext.facet#link",
"uri": "https://www.peachpitmusic.com/"
}
]
}
],
"createdAt": "2025-12-22T16:07:06.094Z"
}
}

Notice that this thread record doesn't mention the forum or topic. It's just content. The thread can exist on its own, be linked by different forums, or stay available even if I leave a community.

Wrapped threads connect user content to community structure.

When I create my thread and select the "General" topic, the community manager creates a wrapper record in the community's repository:

{
"uri": "at://did:plc:dayton-music-scene/community.lexicon.discussion.wrappedThread/2",
"cid": "bafyreiftdoabsi5vi7vhjjulllazad4thunhmxhi2pccqly6t3tkqmjlvy",
"value": {
"$type": "community.lexicon.discussion.wrappedThread",
"topic": {
"uri": "at://did:plc:dayton-music-scene/community.lexicon.discussion.topic/1",
"cid": "bafyreib7phelijh23iktyzraqw54wi7flwthmdzryxscmtvb47cggcfey4"
},
"thread": {
"uri": "at://did:plc:nick/community.lexicon.discussion.thread/1",
"cid": "bafyreihg2rtnzae4wtclusodra5pe4luscjtt66ltwsgwq2zy4jhjhhg44"
},
"createdAt": "2025-12-22T16:07:06.094Z"
}
}

The wrapper contains two strongRefs: one to the topic (community-owned) and one to my thread (user-owned). This is the join point. The forum AppView queries wrapped threads to build the discussion view, resolving the strongRefs to fetch the actual content.

Replies work the same way. Another user writes their reply to their own PDS, and the community manager creates a wrapper linking it to the thread.

With strongRefs, if the content changes the community can decide what they want to do. They could provide a visual indicator that the content has changed, flag it for review, or automatically update the wrappedThread to reference the new CID.

Moderation Without Ownership

This is where the pattern shines. Say someone posts spam or violates community guidelines. The moderators delete the wrapper record (the community's reference to the content). The original thread still exists in the user's repository, but it no longer appears in the forum.

From the user's perspective, their data is intact. They can take their posts to another forum, reference them elsewhere, or keep them as a personal archive. From the community's perspective, they have full moderation control over what appears in their space.

Tombstones and Deleted Content

The wrapper pattern has another benefit: it naturally supports tombstones for deleted content.

If I delete my thread record from my PDS, the community's wrappedThread still remains. The strongRef now points to missing content. Instead of showing an error, the forum AppView can display a tombstone, a placeholder that shows something was here but has been removed.

[Thread deleted by author]
3 replies ...

This keeps the discussion's structure. Replies to my thread still make sense, even if my original post is gone. Other members can see that a conversation took place, that people responded, and that the original author chose to remove their post.

The tombstone can also show metadata from the wrapper. The createdAt timestamp shows when the thread was posted, and the topic reference shows where it belonged. The tombstone could also display "[Deleted thread by @ngerakines.me]" referencing the authority (DID) of the thread strongRef uri.

This is different from how most forums handle deletion. Usually, when content is deleted, the forum decides what happens. Sometimes it's removed completely, sometimes it's replaced with "[deleted]", or sometimes only moderators can see it. The user doesn't get a choice.

With the wrapper pattern, users control when their content is deleted, but the community decides how that deletion appears in their space. I can remove my data from the network if I want to be forgotten. The community can keep the wrapper as a tombstone to keep the conversation's flow. This way, both sides are respected.

The community can also decide to remove orphaned wrappers. A background job could check for wrappers that point to missing content and delete them if the community doesn't want to show tombstones. The AppView has the flexibility to choose.

User-Defined Moderation with Labelers

Community moderation is only half the story. ATProtocol's labeler services give individual users control over what they see, and this should carry through to forum content.

A labeler is a service that adds labels to content or accounts. Users subscribe to labelers they trust, and their client or AppView uses those labels to filter, warn, or hide content. This is user-defined moderation, so you choose whose judgment to follow.

The wrapper pattern preserves this capability because the original record's authority is maintained. When a forum AppView displays a thread, it knows both the wrapper (owned by the community) and the underlying thread (owned by the author). Labels apply to the original content, not the wrapper.

Here’s how this works in practice. Suppose did:plc:loser subscribes to a "USA-ONLY" labeler that flags content about non-American bands. My thread quotes Peach Pit lyrics, which is a Canadian band. The labeler adds a !hide label to my thread record.

When did:plc:loser views the forum, the AppView queries the labeler with my DID and the thread's AT-URI:

at://did:plc:nick/community.lexicon.discussion.thread/1

The labeler returns the !hide label. The AppView follows this, so did:plc:loser doesn't see my thread, even though the community hasn't moderated it and other members can still see it.

This works because the AppView queries against the original content, not the wrapper. The wrappedThread record points to my thread via strongRef, so the AppView knows exactly which record to check. If the AppView queried against its own wrapper record, user-subscribed labelers would be useless - they'd have no way to label community-specific wrapper records across every forum on the network.

The same idea works for account-level labels. If a labeler flags my whole account, that label applies to my threads no matter which community they show up in. The wrapper doesn't hide my reputation.

This layered approach gives forums the best of both worlds. Community moderators set the standards, and individual users can add their own filters. Neither layer cancels out the other.

Scaling Considerations

Forums can get large. The wrapper pattern means the community repository grows with every topic and reply. A busy forum might have millions of wrapper records.

But this isn't a problem. The wrappers are small, just strongRefs with a little metadata. The main content, like post bodies and images, stays in user repositories across the network. The community repository is really just an index, which is what it's meant to be.

Closing Out

Right now, this use case isn't possible because there’s no standard way for one identity to give permissions to others, or for a service to write to a repository for someone else. The community manager pattern, along with the opensocial.community lexicons, provides the needed infrastructure.

If this is interesting to you, reach out with your questions and feedback. Also, check out some of the recent related posts:

The Community Manager Pattern

Representing groups in ATProto

Representing groups and other shared resources in ATProto