There's a particular feeling in the Atmosphere, where developers like myself are free to do whatever in reason. The Protocol is Our Sandbox. I personally feel like this feeling really spawned from the open nature of the AT Protocol, as it birthed many new ideas and projects.

Some examples are Leaflet, the blogging platform I'm using right now, Tangled, a ATProto-Git marriage... a knot? heh. It is a platform that allows git development with ATProto as the social layer. Another example is Anisota, which I haven't used that much but it is an alternate Bluesky client that gamifies the experience. My final example is Teal, a Last.FM rival. It is not technically public yet but you can spin up a Piper instance and start scrobbling already.

I personally made my website rely on the AT Protocol. People may say that it's not a good idea, but I found it to be surprisingly educational.

The Architecture

The entire site is powered by AT Protocol data living in my personal data server. Every single piece of content you see – my profile information, blog posts, music listening activity, code repositories, even the dynamic link boards – all comes from various lexicons in the protocol. The website itself? It's just a view layer. A carefully crafted lens presenting this data in a way that feels personal and, hopefully, somewhat alive.

This wasn't a trivial undertaking, mind you. Building a production-ready website on top of AT Protocol required solving several interesting technical challenges, each one teaching me something new about distributed systems, data resilience, and the real-world trade-offs of decentralised architectures. You know, fun Friday evening stuff.

Agent Management and Fallback Strategies

One of the first challenges I encountered was reliability. The AT Protocol ecosystem has multiple service endpoints – your personal PDS, public API servers, and various third-party infrastructure like Constellation and Slingshot. How do you build something that feels responsive and reliable when any of these services might be temporarily unavailable? Or, as I discovered, when you might have accidentally DoS'd your own PDS at 01:00 because you're a pillock who doesn't implement rate limiting.

I built a cascading fallback system that tries multiple approaches in order of preference. For public data like my profile, it first attempts to fetch from Microcosm's Constellation endpoint for speed. If that's unavailable, it uses Slingshot to resolve my PDS endpoint directly. And if all else fails, it falls back to Bluesky's public API. For private or custom data like my site metadata, it prioritises my PDS first since that's the authoritative source – my data lives there, after all.

The magic happens in the withFallback function – a higher-order function that wraps any AT Protocol operation and automatically retries with different agents if something fails. This pattern appears throughout the codebase, making the entire system resilient by default. When one service hiccups, users never know because the next one in the chain seamlessly takes over. It's rather elegant, actually, which is surprising given how much of this was bodged together over several sleepless nights.

What surprised me was how often this fallback actually activates in production. Distributed systems are inherently unreliable, not because they're poorly built, but because of the fundamental physics of networks. Packets get lost, services restart, rate limits kick in (shocking, I know). The fallback system isn't theoretical defensive programming – it's essential infrastructure that runs multiple times per day. Including, notably, when I borked my own PDS and the fallbacks were the only thing keeping my website functional whilst Bluesky clients struggled. Small mercies and all that.

The Caching Conundrum

AT Protocol data doesn't change that frequently. My profile bio might update once a month if I remember to be witty. My blog posts, when they're published, stay static. But fetching this data on every page load would be wasteful and slow, hammering the infrastructure unnecessarily. The solution? A carefully tuned in-memory cache with time-to-live (TTL) expiration.

I implemented a simple but effective caching layer that stores fetched data for five minutes. This strikes a balance between freshness and performance. Profile data, site information, blog posts, music status – everything goes through this cache. The cache tracks timestamps and automatically invalidates stale entries, ensuring users see relatively fresh data without me hammering the AT Protocol infrastructure like some sort of barbarian.

The interesting part was deciding what TTL to use. Too short, and you're not really gaining much performance benefit – you might as well just fetch fresh data every time. Too long, and users might see outdated information when you publish something new, which defeats the point of having a dynamic website. Five minutes felt right – short enough that publishing a new blog post appears within a reasonable timeframe, long enough that most page loads are instant and don't require network calls at all.

The cache also solves another subtle problem: rate limiting. Many AT Protocol services have rate limits to prevent abuse (which, as I've learnt firsthand, is very important). By caching aggressively, my site stays well under these limits even during traffic spikes. It's respectful to the infrastructure while maintaining great performance. Win-win, really.

The Blog System: Juggling Multiple Platforms

My blog system supports both WhiteWind and Leaflet – two different blogging platforms built on AT Protocol. Each has different lexicons, different data structures, different ways of organising content. The challenge was creating a unified interface that could fetch posts from both sources and present them coherently without making my head explode.

Leaflet supports multiple publications per user – think of them as separate blogs under one identity. I implemented a slug mapping system where friendly URLs like /blog or /essays map to specific Leaflet publication rkeys. The system automatically resolves these slugs, fetches the associated publications, and generates the correct URLs based on whether the publication has a custom domain configured. It's chef's kiss when it works properly.

The URL generation logic is particularly interesting (read: finicky). Leaflet publications can specify a base_path – a custom domain where the blog is hosted. If that's present, we use it. Otherwise, we fall back to Leaflet's /lish/ URL format that includes the DID and publication identifier. This flexibility means I can host my blog on my own domain whilst still leveraging Leaflet's infrastructure. Best of both worlds, isn’t it?

WhiteWind support is more straightforward but required careful handling of draft posts and visibility settings. The system automatically filters out drafts and non-public posts, ensuring only published content appears on the site. This filtering happens during the fetch operation, so there's no risk of accidentally exposing unpublished work. Because that would be embarrassing.

Both platforms generate RSS feeds, but they work differently. Leaflet has native RSS support, so for Leaflet publications, my site just redirects to their RSS endpoint – why reinvent the wheel? For WhiteWind, I generate RSS feeds on-the-fly from the fetched posts. The system is smart enough to check which platform has content and route accordingly. It's surprisingly elegant for something I cobbled together whilst learning how RSS actually works.

Music Integration: A Multi-Source Journey

The music status card on my homepage shows what I'm currently listening to via teal.fm, a Last.fm alternative built on AT Protocol. This seemingly simple feature turned into one of the most complex pieces of the entire system. Because of course it did.

Teal.fm uses two different lexicons: fm.teal.alpha.actor.status for "now playing" status with short-lived expiry times, and fm.teal.alpha.feed.play for permanent scrobble records. My fetcher checks the status record first, validates it hasn't expired (because showing "now playing" from three hours ago would be embarrassing), and falls back to the most recent scrobble if the status is stale.

The real complexity came with album artwork. Music metadata is an absolute mess. Sometimes you have MusicBrainz IDs, sometimes you don't. Sometimes release names are formatted inconsistently. Sometimes artists use different names across services. Sometimes the data is just... wrong. I needed a robust system that could find artwork despite this chaos, because showing a music widget without artwork is just sad.

I built a cascading artwork search that tries multiple approaches:

MusicBrainz Cover Art Archive: If the scrobble includes a releaseMbId, we can fetch artwork directly from the Cover Art Archive. This is the gold standard – high quality, no API key required, reliable. When it works, it's brilliant.

MusicBrainz Search: If there's no ID but we have track and artist names, we search MusicBrainz to find the release. This is where things get interesting. I implemented two search strategies: first try searching by album name if available (more accurate since albums are less ambiguous than tracks), then fall back to searching by track name. Each search returns a confidence score, and we only accept results above a certain threshold to avoid false matches. Because displaying the wrong album art is somehow worse than displaying no album art at all.

iTunes Search API: If MusicBrainz fails, try iTunes. Their search API is free, doesn't require authentication, and has surprisingly good coverage. Plus, iTunes URLs can be manipulated to get higher resolution artwork by replacing the dimensions in the URL path – a neat little trick that makes the art look much better.

Last.fm API: As a final fallback, query Last.fm's album info endpoint. This requires an API key but is freely available for non-commercial use. It's surprisingly reliable for popular music, though obscure releases might not be in their database.

AT Protocol Blob Storage: If all external sources fail, fall back to artwork embedded in the scrobble record itself, stored as blobs in the PDS. This is the nuclear option – it works, but requires knowing the PDS endpoint and constructing the correct blob URL path. Better than nothing, though.

Each source has its own quirks and personality, like dealing with a bunch of temperamental artists. MusicBrainz requires careful user-agent headers and gets upset if you hammer it too hard. iTunes has rate limits but is generally fast. Last.fm works well but needs album names – track-only searches are rubbish. The AT Protocol blob URLs require knowing the PDS endpoint and constructing the correct path, which is fine until you're testing with multiple PDSs and can't remember which one hosts what.

To avoid CORS issues in the browser (because of course there are CORS issues), I built a server-side API endpoint at /api/artwork that proxies all these requests. The frontend makes a single request to my server, and the server cascades through all the sources until it finds artwork. This also allows me to cache artwork URLs server-side, dramatically reducing external API calls and making the whole system faster. The result? Almost every track displays beautiful album artwork, even for obscure releases. And when nothing can be found, there's a graceful fallback to a music note icon. Not perfect, but perfectly acceptable.

Custom Lexicons and Structured Data

Beyond the standard blog and social features, I experimented with several custom lexicons that demonstrate the flexibility of AT Protocol. This is where things get properly interesting, because you're not constrained by what platforms think you should be able to do.

The uk.ewancroft.site.info lexicon stores comprehensive site metadata – my tech stack, privacy statement, open source information, credits, and related services. This isn't just for display; it's structured data that other applications could consume. Imagine a directory of AT Protocol websites that automatically pulls this metadata to create rich listings. It's the semantic web dream, actually implemented and working.

The blue.linkat.board lexicon powers my link board – a curated collection of important links with emoji icons. It's simple but demonstrates how easy it is to define custom data structures and build UIs around them. Need a new data type? Define a lexicon, write some records, build a component. Done. No API approval needed, no terms of service to negotiate.

The sh.tangled.repo lexicon integrates with Tangled, displaying my code repositories with descriptions, labels, and metadata. This is particularly cool because it shows how AT Protocol can bridge different domains – in this case, connecting my social identity with my development work. Your code, your posts, your music – all part of the same identity graph.

Each of these lexicons is a tiny application in its own right, stored as records in my repository, queryable via standard AT Protocol APIs, and rendered by my website however I choose. This composability is powerful. I can add new data types without asking anyone's permission, and other developers can build their own views on top of my data. It's the web as it should be, honestly.

Bluesky Post Display: The Complexity of Social

Displaying my latest Bluesky post seems simple until you consider all the edge cases that would make a QA engineer weep. Posts can have images, videos, quoted posts, external link cards, and rich text with mentions and links. They can be replies to other posts, forming threaded conversations. They can be reposts of someone else's content. They can be all of these things at once.

My implementation handles all of these cases, or at least tries to. It fetches the latest post from my feed, recursively loads parent posts if it's a reply (up to a certain depth to prevent infinite recursion), fetches quoted posts up to three levels deep (because yes, people quote posts that quote posts that quote posts), extracts and displays images with alt text, embeds videos with HLS support, and renders external link cards with thumbnails.

The facet processing is particularly interesting (read: a massive pain). Facets are byte-offset annotations in the post text that indicate links, mentions, and hashtags. Converting these byte offsets into clickable elements requires careful handling of Unicode characters (which can be multiple bytes) and HTML escaping. Get it wrong, and links point to the wrong places or break entirely. Get it really wrong, and you've accidentally created an XSS vulnerability. Fun times.

I also integrated with Constellation's engagement API to display accurate like and repost counts. The official AT Protocol doesn't provide easy aggregation of engagement data (it's all distributed, after all), so Constellation fills that gap by indexing likes and reposts and providing efficient count queries. It's a clever solution to a hard problem, and it works remarkably well in practice.

Actually Owning Your Stuff

What makes this approach powerful is the fundamental separation of concerns. The AT Protocol handles identity, data storage, and federation. My website is just a view layer – a personalised interface on top of my data. This has profound implications that honestly weren't obvious until I built the thing.

If I wanted to switch hosting providers tomorrow, I could deploy the same code anywhere. The data stays in my PDS; only the frontend moves. If I wanted to build a mobile app with the same data, the backend work is already done – I just fetch from the same AT Protocol APIs. If I wanted to grant another developer access to build their own view on my data, I could do so without giving them access to my hosting infrastructure or database credentials.

This is true data portability. Not the kind where you export a ZIP file and hope it works somewhere else (looking at you, GDPR exports), but the kind where your data has a stable, protocol-defined location and any application can read it with your permission. Your data isn't trapped in a platform's database; it lives at a cryptographically-verified address that you control.

Nothing Is Perfect, Obviously

Of course, this approach has trade-offs. You're dependent on PDS availability, though the fallback chains I built make this manageable in practice. Cache invalidation is always tricky – there's an inherent delay between publishing something and seeing it appear on all views. You need to be thoughtful about rate limiting (as I learnt the hard way), though caching helps significantly.

There's also a conceptual complexity cost. Traditional websites have a database that they query directly. My website queries a distributed network of servers with fallback logic. The happy path is simple, but the edge cases require careful thought. It's more moving parts, more potential failure modes, more things to debug at 02:00 when something inevitably breaks.

And there's the question of search engines and SEO. Server-side rendering helps, but you're still fetching data from external services during page generation. I solved this by using SvelteKit's load functions to fetch data server-side, rendering complete HTML that search engines can index, but it's an extra consideration. Traditional CMS platforms have this figured out; with AT Protocol, you're pioneering new territory.

Expensive Lessons in Distributed Systems

Building this taught me more about distributed systems than any textbook could. I learnt about resilience through experience – seeing which services fail under which conditions, understanding the importance of timeouts and retries, appreciating the complexity of seemingly simple operations like "fetch a user's profile." Turns out "fetch profile" is actually "resolve identity, find PDS endpoint, establish connection, make request, handle timeout, retry with different endpoint, cache result, hope nothing explodes." Simple!

I learnt about the importance of caching not just for performance but for reliability. When services are temporarily unavailable, cached data keeps the site functional. When APIs have rate limits (or you've accidentally DoS'd yourself), caching keeps you under them. Caching isn't just an optimisation; it's essential infrastructure that makes the difference between a working site and a pile of 500 errors.

I learnt about the messiness of real-world data. Music metadata is inconsistent. Artwork URLs break. Records have slightly different structures depending on which client created them. Search results are ambiguous. Building robust systems means handling all of this gracefully, with fallbacks and defensive checks at every layer. Assume everything will be wrong, and you'll be pleasantly surprised when it occasionally works.

Most importantly, I learnt about the freedom that comes from true data ownership. My content lives in my repository. My identity is cryptographically mine. I can build whatever interface I want without asking for API access or worrying about terms of service changes. This is the promise of AT Protocol made concrete, and it's genuinely brilliant when you see it working.

The Future

This website is a proof of concept, but it points toward something larger. Imagine a world where your social graph, your blog posts, your photos, your projects, your interests – all of this data lives in your personal data server, queryable via open protocols, and you can authorise any application to build interfaces on top of it.

You could have a professional portfolio site that shows your code repositories and technical blog posts. A personal site that shows photos and casual updates. A mobile app for quick posts on the go. An analytics dashboard showing engagement metrics. All built by different developers, all working with the same data, all controlled by you. Not by platforms, not by algorithms, not by advertising companies. You.

This isn't hypothetical sci-fi nonsense. The infrastructure exists today. The AT Protocol provides the foundation. Projects like Leaflet, Tangled, Teal, and countless others are building the applications. My website is just one small example of what's possible when you take data ownership seriously.

The Bigger Picture

The AT Protocol isn't just a technical specification; it's a philosophy about how the internet should work. It says: your data is yours, your identity is yours, and you should be free to build whatever you want with it. It says: protocols are better than platforms, and interoperability is more valuable than lock-in. It says: the future of the web is decentralised, user-controlled, and built on open standards.

My website is a small manifestation of these principles. Every piece of data comes from open protocols. Every custom lexicon is documented and could be implemented by others. The entire codebase is open source. There are no proprietary APIs, no vendor lock-in, no black boxes. It's web development as it should be – open, accessible, and built on shared standards.

Sure, there are challenges. Distributed systems are inherently more complex than centralised ones. But the benefits – true data ownership, application diversity, user control – far outweigh these concerns. And as more developers build on these foundations, the rough edges will smooth out, best practices will emerge, and the experience will become seamless. We're in the early days, but the direction is clear.

Why It Matters

Building this website changed how I think about the web. For years, we've accepted that our data lives in corporate silos, that switching services means starting over, that we're subjects of platforms rather than owners of our information. AT Protocol offers a different vision, and it's not just theoretical – it works.

When I publish a blog post, it's mine. Not in the sense that I have a local copy (though I could), but in the sense that it lives at a stable, cryptographically-verified location that I control. When I scrobble a song, that data is mine to query, analyse, and display however I want. When I post on Bluesky, I'm not just adding a row to Twitter's database – I'm creating a record in my repository that I can access directly, that other applications can read, that will exist as long as I want it to.

This ownership creates possibilities. I can write custom analytics tools that analyse my posting patterns without giving some company access to my data. I can build recommendation engines that understand my music taste without training someone else's algorithm. I can create archives, visualisations, integrations – all without needing anyone's permission. The data is mine, and the AT Protocol makes it accessible. It's genuinely liberating.

Conclusion

Was it a good idea to build my entire website on AT Protocol? People might question it, and honestly, sometimes I question it myself at 03:00 when something breaks and I'm trying to debug cascading fallback chains. It's more complex than a traditional CMS. It requires understanding distributed systems. It has edge cases and failure modes that simpler architectures don't have.

But it's been one of the most rewarding projects I've built. It forced me to think deeply about data architecture, resilience, caching, and the practical realities of building on decentralised protocols. It gave me a visceral understanding of how AT Protocol works, not just as a spec but as lived infrastructure that you have to coax into behaving.

More than that, it's a statement of belief. I believe in user-owned data. I believe in open protocols. I believe in a future where applications are interfaces on your data rather than guardians of it. Building my website this way is a small act of defiance against the walled gardens of the modern web, and a small step toward what the internet could be. Should be, really.

The AT Protocol is our sandbox. It's a space where we can experiment, build weird things, break stuff (hopefully not your PDS at 01:00, but here we are), and figure out what the future of the web looks like. It's not perfect. It's early days. There are rough edges and missing pieces and moments where you wonder why you're doing this instead of just using WordPress.

But when it works – when your music widget updates in real-time, when your blog posts flow seamlessly from Leaflet to your site, when your fallbacks catch a failing service and users never notice – it's magic. It's the web as it should be: open, federated, owned by users rather than platforms.