Jeremy Herve Avatar

That’s me. And my blog. In English and in French.


Is my ATProto app actually decentralized?

A few days ago I wrote about rolld, the little board game tracker I built on the AT Protocol. As I mentioned then, this is very much a learning experiment for me, a way to play with ATProto and learn about it. In this post, I’d like to come back to a few of my struggles and the things I learned while designing it.

ATProto is supposed to be about decentralization. But the app I was sketching out didn’t feel decentralized at all:

  1. Every record that gets posted on a user’s PDS means nothing without the central record in my app, and ultimately without BoardGameGeek, since rolld’s game pages are powered by cached data from BGG.
  2. How do I save board games themselves? A single board game can’t be something each user creates on their own PDS, because then you get a duplicate “Wingspan” for every person who’s ever played it. But it also can’t be a record that lives on a single PDS (say mine), because then it’s centralized all over again.

So a “game” either gets duplicated across everyone’s PDS, or it lives in one central place and re-centralizes the whole thing. Both felt wrong, so I had to take a step back.

It turned out to be a false dilemma

What I’d missed is that there was a third option.

The way out was to stop trying to model “the game” as a record at all. What if there’s no central “Wingspan” record anywhere? What if there are only per-user activity records, one for each thing you’ve played, and each one carries a small snapshot of the catalog facts it needs (the game title, the year it was published in) right inside your own record?

So the answer to “duplicated or centralized?” was: neither. There’s no shared record for the thing. There are only activity records that point at the thing by a shared identifier, and carry whatever snapshot they need to stand on their own.

My mistake was assuming a “game” had to be a record somewhere. It doesn’t. The game is just an ID everyone agrees on.

The three layers

Once I clearly separated the 3 layers of my app, most of my “wait, is this decentralized?” worry got sorted out.

Layer 1: shared identity. Everyone refers to a game by its BoardGameGeek ID. This feels like centralization, but mostly it isn’t. Agreeing on an identifier is not the same as depending on a server. “BGG ID 266192” is just a string, a name, like an ISBN for books. Libraries shelve books by ISBN and nobody calls libraries centralized. A shared namespace is the coordination point that lets different people’s records be talked about together. The ID remains unique and keeps working even if BGG vanished tomorrow.

Layer 2: swappable metadata. The name, the cover art, the complexity rating, and the community rating all come from BGG metadata right now. That said, I only view that information as a way to improve the display of a game. It’s not the source of truth about anything that matters in my app. Whether a cover loads from BGG, from Wikidata, or not at all, my record of having played that game with my friends on Tuesday is unchanged and remains mine. In practice, metadata gets hydrated at read time through a small resolveGame() function. All of BGG’s centralization lives in this layer. Tomorrow, I could swap BGG by another provider in that function, and I would be good to go. On top of that, and since BGG is the main source of information about board games today, I can almost guarantee that another provider would carry the BGG ID along with the rest of the metadata, making it easier to match metadata and games.

Layer 3: owned activity. your play logs and your ratings. These live signed on your own PDS, portable, readable by any AppView. This is the layer I really want to be decentralized.

The reason the app felt non-decentralized is that I was judging it by Layer 1 and Layer 2, the layers that I think are supposed to be shared and external, instead of Layer 3, which is the layer decentralization is actually about.

Reframing the core principles of my app

After switching my view to those 3 layers, I took another look at how my app should be designed.

My app is just a lens, not a source of truth. I’d told myself the records mean nothing without the central record in my app. But that’s not what the AppView is. If rolld disappeared tomorrow, every play record still exists on every user’s PDS, still says “I played this game with these people on this date,” still readable by any other app that understands the format. What rolld provides is aggregation and display: the web page, the “who else played this” view. That’s the AppView’s job everywhere in ATProto. rolld being needed for the nice view doesn’t mean rolld is needed for the data to exist or to mean anything.

Can my app be replaced? I figured this was a good way to test the actual decentralization of my app. Since my app is just a lens, any other app could come along and use the existing data saved on everyone’s PDS. That seems decentralized enough to me. It’s not really about my app’s external dependencies; I don’t think we can reasonably expect a social app like rolld to work without any external data points.

“BGG-dependent” versus “BGG-enriched.” Instead of asking myself “do you depend on BGG?”, I choose to ask myself “if BGG vanished, is anything users own lost?” The answer is no. All users would lose is live cover art and other metadata, until someone pointed the resolver at another catalog. People would lose nothing from their records.

In practice, it did force me to review and change my data model a bit. To make the records readable on their own, I decided to store a minimal name snapshot in each record: { id, name } (plus an optional year) rather than just { id }.

  • The ID is the unique reference (matching BGG game ID), building links and linking everything together.
  • The name snapshot is so it still reads as something a human logged. It’s “what I called the game when I logged it,” captured at write time, so the record stays readable even if BGG dies, if rolld is down, or some future AppView comes along that has never heard of BGG.

Live hydration (game covers, complexity, number of players, time, community rating) is just pure enrichment on top of a record that already means something on its own.

I purposefully stored very little, because I didn’t want to save everything and end up with:

  1. outdated data in PDS records
  2. basically rebuilding BGG’s index inside everyone’s PDS; that’s not the point.

What I’d tell someone else

If you’re new to ATProto and you hit this same wall, here are 2 things I wish I’d known going in.

  1. The first is that “decentralized” is a property of the data layer, not of the whole app. I was applying the word to rolld as one big monolith, when it really belongs to Layer 3, the owned records. Shared vocabulary and external metadata being a bit centralized is fine and expected i think. Once you know which layer to point the word at, most of the panic goes away.
  2. The second is to watch for false dilemmas shaped like “duplicated or centralized.” I was stuck with this because I started with a bad premise: “a game must be a record somewhere.” Once I took a step back and reframed things a bit, it became easier to design the app.

It’s not perfect

It’s not a perfect solution though; BGG-enriched is still a dependency for nice display. If BGG went down tomorrow, rolld’s game pages would look pretty bare until I pointed the resolver somewhere else, and I haven’t built that fallback yet. The catalog metadata is still someone else’s, and I’m still leaning on it. But I think it’s important that I moved it out of the layer that matters.

If you’ve hit this same wall building on ATProto, I’d love to hear how you thought your way out of it. And if you missed the first post, you should probably go check rolld.at!

Reactions on the Fediverse and on the ATmosphere

Leave a Reply

Your email address will not be published. Required fields are marked *


Continuez votre lecture / Keep reading

Discover more posts about or look at some of the posts suggested below.

Jeremy Herve
Jeremy Herve

WordPress, TV Series, music, kids, and board games. I think that’s probably the best way to define me in a few words. 🙂

I work at Automattic where I lead a team building tools for bloggers and creators. I talk a lot about WordPress things, but also about all things open source in general.

I post in English and in French.

I live in Brittany, France, so you’ll also find me sharing pictures from our beautiful region from time to time.

1,244 posts
196 followers