oofblog

Blacksky's community posts are centralized

a mostly technical complaint

February 07, 2026

Epigraph

“It would seem that to continue to see race of people, any race of people as one single personality is an ignorance of gothic proportions, an ignorance so vast, so public, and perception so blind and so blunted, imagination so bleak that no nuance, no subtlety, no difference among them can be ascertained.” - Toni Morrison

I started with this quote because recent reactions to criticism of Blacksky, some of which not only calls out people for the color of their skin but also makes assumptions as to what that color is, feel like a return to the ignorance Morrison talks about.

This article is not a critique of community posts, but rather of the specific implementation of them in Blacksky. I think work needs to be done on features like this at the protocol level where everyone benefits from it, rather than the app level where they fragment us.

With that out of the way, let's discuss why what Blacksky is doing is a problem for everyone that cares about this protocol and its future, regardless of their race:

What happens when you post on Bluesky?

To understand why this is an issue, first you need to understand what makes Bluesky different than other social media, and one good way to do that is to follow the life of a post on both Twitter and Bluesky:

Twitter

When you post on a centralized service like Twitter, that service holds custody over your data, controlling who can access it and how, as well as your ability to manage it.

Twitter decides what posts are allowed, how much reach they'll get, who can engage with them, because they are the ones that own your data, keeping it in their own private database, and making it accessible only through methods they endorse, in this case only through their official app and website.

They get to use your data however they want, and share it with whoever they want, in whatever order they want, with you having no say over it. The point of Bluesky is to give the power to control their data and feeds back to the user:

Bluesky

When you post on Bluesky, your post doesn't actually get sent to Bluesky directly, instead it goes to something called a Personal Data Server (PDS), which any client can then access.

As the name suggests, this PDS is something you have control over; you can at any point add, edit, backup, or remove any data and switch PDS providers (or self-host your own!) without any negative reprecussions. Your data on Bluesky and other ATProto services are completely yours to do with as you please, without any middlemen!

Here is an example of what a post record looks like:

```

{
  "uri": "at://{userID}/app.bsky.feed.post/{key}",
  "cid":"bafyreie25lba7tr43gqijxhme5npakdytiicxpeacmud5vrc7zwaho55gm",
  "value": {
    "text": "this is a post :)",
    "$type": "app.bsky.feed.post",
    "langs": ["en"],
    "createdAt": "2026-02-08T00:51:46.094Z"
  }
}

This is what gets saved in your PDS, and as you can see, this structure contains the post's content, when it was created, and its language. (In more complicated posts it would contain more info, but that's besides the point.)

What really matters, is that since any appview can subscribe to your PDS, it also means your followers are free to browse your posts through whatever client they want, or that you can build your own without worrying about hosting and while keeping users in full control! And if something happens to Bluesky, then no problem, you can just use another AppView, without losing anything!

It's a win-win for users and developers alike, and it's why projects like Blacksky were even possible in the first place!

So how do Blacksky community posts work, then?

And so we get to the crux of the matter. How does this new post type work, and why is it a problem for the protocol? Well, let's look at the code! In the code to create a post we see the following:

// Step 1: Build the canonical record for CID computation
// This MUST match the structure the appview uses for CID verification
const createdAt = now.toISOString()
const canonicalRecord: Record<string, unknown> = {
    $type: COMMUNITY_POST_COLLECTION,
    text: rt.text,
    createdAt,
}

/** ... **/

// Step 2: Compute CID locally - this is the SOURCE OF TRUTH for integrity
const cid = await computeCid(
    canonicalRecord as unknown as AppBskyFeedPost.Record,
)

The code above computes a Content IDentifier (CID), which ATProto uses to verify that you are the actual author of a post, and that it hasn't been tampered with. Normally, the PDS would do this for you as the source of truth, but for these posts it's handled clientside.

Afterwards, their own submitPost endpoint is called:

const submitRes = await communityXrpc(
    agent,
    'community.blacksky.feed.submitPost',
    {body: submitBody},
)

/** ... **/

// Appview returns the verified CID - should match our local computation
const submitData = await submitRes.json()
if (submitData?.cid !== cid) {
    logger.warn(`CID mismatch: local=${cid}, server=${submitData?.cid}`)
}

Note here that this submits your post to their backend at https://blacksky.app instead of your PDS, the same way a centralized platform like Twitter would.

There's also a comment in the code that straight up says they're loaded from their Appview instead of your PDS:

// Handle community posts differently - they need to be fetched from appview
if (replyToUrip.collection === COMMUNITY_POST_COLLECTION) { /** ... **/ }

Aside: There is something written to your PDS.

For the sake of completeness, they do also add a stub record to your PDS, however their record only contains a CID that is used in the process of loading your posts from their private database:

{
    "uri": "at://did:plc:w4xbfzo7kqfes5zb7r6qv3rw/community.blacksky.feed.post/3me5kmra4p22g",
    "cid": "bafyreibwvmiczbd3u4rp4l2jfpb346yfksi56ggsdn7dqqb2tjb5ncw4me",
    "value": {
        "cid": "bafyreibhspfb3jxatdjavwnsjroiv5pvu6g5o6jw57vi7xn6go7wfypsj4",
        "$type":"community.blacksky.feed.post",
        "createdAt":"2026-02-05T23:36:48.101Z"
    }
}

As you can see, the part you control only contains an ID to the post so it can be retrieved from Blacksky's private database. Everything else is out of your hands.

What does this mean

If we were to make a diagram of what we just learned about how community posts on Blacksky work, it would look like this:

This means people might have to use Blacksky if they want to see some of the posts of people they follow on Bluesky, or Northsky, Wafrn, AppViewLite, basically anyone on any other part of the ecosystem will not be able to see these posts, even if you want them to. (update: there might be a way to implement this, see the apppendix at the very end)

And while you can delete the stub record from your PDS there's no guarantee that Blacksky will delete your post from their own private database. And if Blacksky decides no one should be able to see what you posted, then you don't get to switch your moderation service to see it. And of course that also gives them control over your feed in general. It's not hard to imagine a bad actor buying them out or doing a hostile takeover and manipulating your feed, no different to Elon Musk.

This implementation sets Blacksky up as a single point of failure, and runs counter to the whole idea of ATProto, that users control their data, their feeds, and their apps.

And when other communities implement their own system in the same way, well, you'll just end up with siloes rather than an open network, much like how other decentralized protocols turned out.


I do not think that anyone working on this is evil, and I think the idea of community posts is perfectly fine, but I also think that this implementation is extremely short-sighted, especially considering how other decentralization efforts like XMPP turned out due to proprietary extensions like this.

I think we all love the potential of this protocol, and I think we're all extremely tired of how slowly the standards have been progressing and issues that remain open after months and even years.

And, to some extent, the Bluesky team themselves is guilty of this, with bookmarks and DMs being implemented in centralized ways.

But this implementation of community posts cross a new line--centralizing content, and not doing that is the whole idea behind ATProto to begin with. And this coming from one of the organizations that until now has steadfastly been a positive influence on the protocol is extremely concerning. And so, as a citizen of this protocol, I felt I had to say something about it.

If this pattern continues, we'll just have built Twitter with extra steps.

I don't think we want to let that sink in.


Appendix 1: I misunderstood how hydration works

There might actually be a way that other appviews could show Blacksky community posts. Basically, the idea is that the Blacksky feed generator sends these posts just like normal Bluesky posts in the hydrated feed view.

Bryan Guffey (they/them)'s avatar
Bryan Guffey (they/them)
13h

Your post also implies you must use the Blacksky client to see community posts. But hydration in ATProto doesn’t happen in the client — it happens at the AppView. The Bluesky mobile app is a thin renderer. It gets back fully hydrated FeedViewPost objects and just displays them.

I am still concerned about scale and compatibility, especially if more services implement their own post scopes like this, but it's an interesting prospect.

And I replied to every criticism in that thread in detail here: https://bsky.app/profile/chaosgreml.in/post/3medbq36i4c2i

I learned a lot!

Subscribe to oofblog
to get updates in Reader, RSS, or via Bluesky Feed

atproto
bluesky