I built the same avatar builder feature three times this week! The third one ended up being the really obvious ecosystem answer, so I thought I'd write it up how I got there.

The setup

atmoBB is a (very new) phpBB-style forum that runs on top of atproto. Boards, threads, signatures, post counts, basically everything you remember. Avatars matter a lot on forums, and a cropped photo is fine, but I wanted to make a real old school avatar generator!

So I bought a pack. RhosGFX Vector Avatars Pro: 1105 SVGs, lots of very cute combinations. Everything stacks on top of each other. A finished avatar is a very small block of JSON:

{
  "v": 1,
  "skin": "brown",
  "hairFront": { "shape": "1", "color": "darkBrown" },
  "eyes": { "shape": "1" },
  "top": { "type": "suit", "color": "green" }
}

The builder took an evening and it was a delight after 20 years of building the Subeta avatar builder 😅 (which has over 60,000 items and millions of combinations).

Whoops

I went to put atmoBB on GitHub and reread the license for everything I was uploading. I hate to steal (from artists)!!!

No redistribution on this one set of assets. The pack can't go in a public repo or a distributable package. It can live on a site I run. atmoBB is MIT and public, so the artwork and anything that use the artwork had to be in a repo far far away.

I'm pretty sure I read the license when I bought it, thought "later," and built the builder because it seemed really cute.

Try two: a plugin system

I built a build-time plugin hook. A trusted package outside the checkout could add a settings page, an avatar renderer (and really just an avatar renderer lol), and generated public assets, and Vite would compile all of it into the build with the forums core package. The builder moved to a private repo. The artwork moved to a directory on the server. An ATMOBB_CONFIG env var pointed at a config that wired them together. The docs had a four-directory deployment layout:

/srv/atmobb/            public core checkout
/srv/atmobb-plugins/    installed plugin packages
/srv/atmobb-private/    licensed or private plugin inputs
/etc/atmobb/config.mjs  deployment-specific plugin selection

There were virtual modules like virtual:atmobb/plugins/avatar-client. My deploy dashboard has a "build source" feature that clones a private repo into vendor/ between clone and build, so the licensed stuff never touches the public checkout. About 600 lines in core just so I could have an avatar builder, a docs page, and a caution box I wrote myself:

A plugin can do anything atmobb can do. Setup code runs with the build user's permissions, and server modules run inside the atmobb process alongside your OAuth sessions and your database connection. There is no sandbox.

After looking at the code again it was clear that I'd built a plugin system just for my single use, opened up a can of worms that I could, in fact, close at the moment. I deleted all of the code, and instead leaned in to the atproto way of having my cake and eating it too.

The obvious network thing

The whole idea of atproto is that the user's data lives in their PDS repo, not in the app (mostly). Any app the user authorizes can write there. atmoBB only ever needed to read the avatar blob.

So the avatar builder is its own app now. A static site at atmobb-avatar.keith.is, with no server and no appview. You log in with your atproto account through browser OAuth and it asks for exactly this:

atproto repo:app.atmobb.actor.profile repo:is.keith.avatar.recipe blob:image/png

Two collections and PNG upload. The recipe goes in its own record, is.keith.avatar.recipe, so that you can come back to it later. The saved PNG goes into the avatar field on the atmoBB profile record, the same field the regular avatar uploader on any atmoBB forum uses. Saving is a read-modify-write on the profile with swapRecord set to the CID it read, so if you edited your bio on the forum a second earlier, the save fails instead of eating it.

The forum has no idea the builder exists. It doesn't need to! 🤯 Every atmoBB forum gets the result. If somebody else builds another avatar app tomorrow and it writes the same blob, that will work too!

The plugin creates a PNG at about 9 KB. The new app bakes 512 by 512 at about 20 KB, because the lexicon allows a megabyte and there was no reason not to.

RIP Plugin system

Deleting the plugin system from atmoBB was a lot of code (634 lines!) removed. The avatarBuilder field stays in the profile lexicon marked legacy so existing records still validate. The new app reads it once, if you have one and no recipe record yet, and never writes it again.

One new env var. 🆕 ATMOBB_AVATAR_BUILDER_URL puts a "Build a cartoon avatar" button next to the photo upload, which is completely optional. That is the entire integration now!

Sooooo

I need to remind myself to think from the 🌌 network perspective from the start before I go and do something silly like build a plugin system for a single use and then have to rip it all out publicly.

And make more little microsites that don't require a backend and play around with the users PDS values 👹 (with permission)

Find me on bluesky, and my cute forums