Porxie - a correct, fast ATProto blob proxy
Building a self-hostable ATProto blob proxy for apps and communities, furthering the decentralised web.
’ve been quietly working on a new ATProtocol social platform since the start of 2026. One of the pieces I needed to build for it was a blob proxy, and towards the end of February I decided to spin it off into its own independent project as I believed it had greater potential than being tied to one application. After a few weeks of work and some discussion with a knowledgeable friend, that new project eventually became Porxie.
What is a blob proxy, though?
A blob is just binary media - images, videos, that kind of thing. In ATProto, blobs are typically stored on a Personal Data Server (PDS) and associated with user records. For example, when you upload an image with your Bluesky post, that image gets stored on your PDS alongside the record and is referenced by a CID (Content Identifier), which is a content-addressed hash of the blob’s contents. When clients later want to display that content, they’ll need some way to retrieve it.
A blob proxy sits between the client (usually the user) and the source. Instead of clients requesting blobs directly, they go through the proxy, which will typically fetch the blob, maybe do some validation and caching, then serve it. Most ATProto applications tend to prefer this over going direct, for reasons such as:
-
Security and privacy: requesting a blob directly leaks the client’s IP address alongside other sensitive metadata such as browser information and location. There’s also no chance for an intermediary server to perform security checks before content reaches the user - you have to explicitly trust the source.
-
Reducing network load: proxies can cache blobs so that repeated requests from multiple users only result in a single upstream request, reducing load on PDS operators and identity services.
-
Improving response times: proxies pair very nicely with Content Delivery Networks (CDNs) which reduce the time it takes to load content worldwide by caching it globally. If a blob is hosted on a PDS in Singapore and the client is requesting it from England, a proxy + CDN leads to a massively improved response time.
-
Availability: a proxy (or any layer in front of it) with a warm cache can continue serving content even if the upstream source goes down - important in a decentralised environment where any host can have outages.
-
Moderation: a proxy can apply takedowns and restrictions to content. For example, an app that does not allow NSFW content can choose to not serve it, ensuring it cannot be reached through their service directly. This is one part of many in the ATProtocol’s ethos of “stackable moderation”.
Why not Bluesky’s CDN, why build your own?
Almost every ATProtocol application right now uses cdn.bsky.app to serve blobs on their behalf. It works, but there are a few things about it that can be genuine issues.
Bluesky controls how it operates and builds it to fit their platform, and when they make changes to how it works or what it serves, every application depending on it has to adapt too. It also isn’t open source, which is totally fine and Bluesky’s call to make, but I believe infrastructure this foundational and this widely relied on should be as transparent as possible. When you’re building on a decentralised protocol, a closed-source corporate-run piece of infrastructure can be quite a fragile thing to depend on.
There are implementation problems too. For example, at the time of writing, it does no validation on blobs and will serve content that doesn’t match its CID, which allows for modified or potentially malicious blobs to be served undetected.
There are also operational limitations that cannot really be worked around due to the nature of it being a shared service: there’s no way to apply your own moderation policies, and if you need to remove content - following a platform ban, a PDS takedown, or a user deleting their content - you can’t purge the cache yourself, you’re just left waiting for it to expire. It also only serves static images via a fixed set of transform presets, with no way to serve alternative media types or use your own transforms.
There’s basically no open alternative out there that does what’s needed and does it correctly, so I decided to build one - and after a few weeks it became clear it was worth releasing as its own project rather than keeping it buried in a larger codebase. Having more independent, app-specific or community-run options matters a lot for the ecosystem; the less we all depend on a single provider for something this fundamental, the better.
So, what does Porxie offer?
Porxie offers simplicity, speed and correctness. At its core, Porxie is just an HTTP GET endpoint: give it a user’s DID and a blob CID and it will find the right upstream source, fetch the blob, validate it, cache it, and serve it back. A few things I cared about getting right:
-
Proper CID validation: The CID in the request URL is a hash of the blob’s contents. If the content doesn’t match the hash, Porxie won’t serve it. Without this, a malicious PDS, blob storage host, or someone with disk/network access can silently substitute blob content without the client knowing.
-
Content security headers: Browsers can be very eager to execute or render content in unexpected ways. Serving user-generated content without strict headers can lead to scenarios where a blob gets interpreted as something it shouldn’t, and in the worst case, steal credentials or compromise the viewer’s machine.
-
MIME sniffing and filtering: Rather than trusting the content type reported by the PDS, the proxy will sniff it directly from the blob’s contents, falling back to
application/octet-streamwhen it can’t be determined. From there the proxy operator can restrict which MIME types are allowed to be served, including whether the fallback can be used. -
Policy service: Porxie can integrate with an external policy service to decide whether a blob should be served, which is where you can run your own validation or moderation logic.
-
In-memory caching: Hitting upstream services for every request is wasteful. Porxie caches expensive operations with configurable lifetimes to reduce load on upstreams and improve response times. The cache can be purged on demand via an authenticated endpoint, useful for things like account takedowns or content removals.
Porxie also pairs well with other tools. For example, using Porxie together with imgproxy ↗ gives you a full image transformation pipeline - resizing, format conversion, cropping - on top of Porxie’s validated blob fetching. This means you can even build a setup that replicates cdn.bsky.app’s URL scheme and image presets if you want, and a rough example for doing so can be found in Porxie’s documentation.
What’s next for Porxie?
Porxie is currently pre-v1.0 as there are a few pieces I want to solidify before making a full stability promise. While all the core functionality is done, I’d still like to work on some nice-to-have features like:
-
Automatic cache eviction: Porxie should listen for account & identity events (such as PDS account takedowns, account migrations and more) and automatically clear the relevant caches.
-
Metrics & improved logs: it’d be great to expose a metrics endpoint with useful data that plugs into tools like Grafana, exposing metrics like cache hit rates, policy block rates, response times and more.
-
Support for alternative upstreams: right now Porxie will always fetch blob content from the user’s PDS - however there may be times where the source of truth for a blob may actually be a separate service entirely, or the client may want to manually specify the upstream. This is likely to be done by a query parameter.
After releasing v1.0, I’d like to investigate migrating calls to use XRPC as is the chosen way for cross-service communication in the ATProtocol, it would make sense to build on top of that and provide a strictly typed schema that services can implement. This will most likely be done for calls between Porxie and the policy service first as the most benefit for strict typing can be found there; it may also be implemented for other core service endpoints, however that is still being thought about. This is likely to be more of a v2 feature than a stable release feature.
How do I get started?
Everything you need to get started is available in Porxie’s Codeberg repository ↗ (mirrors: Tangled ↗, GitHub ↗) - the README will guide you through getting set up and any configuration steps.
If you do use Porxie and run into issues or have suggestions, feel free to open an issue - just note that the scope of the project will not expand that much further. The hope is that things will just work, and they will work well.
Did you enjoy this article?
Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.