AHOY! 2025 - Samuel Newman with How we added video to Bluesky

Samuel Newman ( @samuel.fm ) gives a talk at AHOY! European Social Web Day in Hamburg in 2025 about How we added video on Bluesky.

Thank you. Yeah — this — should I wait for the thing to appear? But yeah — I’m Samuel. I work for Bluesky — and I have for about a year now, just over.

I wanted to cover in this talk — how videos work on Bluesky — because it’s a little bit complex and convoluted.

So — how do we add videos to this guy? This is not a video — it’s a presentation. It’s different. Not my specialty.

As I was saying — I’m Samuel. I work at Bluesky. When I joined a year ago, we’d just launched — we had a billion folks using it — but it wasn’t feature-complete yet. We were still in a bit of a folk in the road. We had capacity to do videos and DMs at the same time — we had to pick one — and we picked DMs. Then we built video.

I wanted to first cover our process for building features at Bluesky — because we’re a pretty tiny team. At the time, it was under 15 people — now we’re just over 20 — but building an app at that scale is difficult.

Our process: each project has a DRI — Directly Responsible Individual — who owns the feature and shepherds it through — keeping the project up to date, syncing with leadership, running meetings — that kind of thing.

In video’s case — it was a big project — front end and back end. On the front end — it was Haley and I — adding a video player to the app — which is quite complicated. That was its own sub-project. I worked with Devin on the back end — which is mainly what I’ll be talking about today.

First — we plan thoroughly. We write a doc — figuring out what we want the feature to be — and what we can do. Building decentralized systems is difficult — we have different constraints than normal applications.

We did a lot of fact-finding — figuring out how much we could shift onto the client. Turns out — web browsers are not very good at video processing yet — so it ended up being a lot of processing on the server. But we needed to find that out — so we wrote a bunch of docs before we started typing.

We built it — small team — four to five people working on this out of a total of 15–20. Then we launch — and repeat endlessly — until we’ve built an entire social media app.

So — what’s the plan?

I need to give a bit of background on how blobs work on AT Protocol.

We’ve got three major parts that work together: the PDS — where your data lives — the App View — which collates everyone’s PDS data and turns it into a social media app — and the client — which is the app you look at.

Say I want to post a selfie. First — I upload the selfie JPEG to my PDS — then I write a record that references this selfie.

In pseudo-code — I’m just like: selfie.jpg. The problem? What if I upload two selfies — both called selfie.jpg? How does it know which is which?

We solve this by using a hash function to hash the image. A hash function — if you don’t know — takes data — usually a string — but you can also do it with images — and deterministically generates a unique hash. Run the same image through it again — you get the same output. Run a different image — you get a different output.

We can use that hash as its name — its true name — not just what you called it — but what it actually is. We pass our selfie through — and get its actual name — a long string — which is the content ID. That’s what you see in AT Protocol — it’s the CID — content ID — found by hashing the content itself.

So — we now name the file by its hash — and it’s guaranteed to be that image.

This comes with interesting properties:

A file only has one name — no more selfie (1).jpg — no confusion.

You can verify the image — if someone gives you a blob — you can hash it yourself — and confirm it’s the right one. This is crucial in a trustless network — you don’t need to trust the CDN — you can verify it yourself.

You can find any blob on the entire AT Protocol network — just from the data and the CID. The CID points to a PDS — the DID points to a PDF — and the image lives on the PDF. So — with a DID and CID — you go to the PDS — ask for the blob — and it gives it to you. You can find any image super easily.

In fact — I wrote a CDM in just 74 lines of code. It’s on GitHub — doesn’t do the validation step — which is important — I recommend you do that in production — but it’s super simple. I was shocked how easy it is to write an image CDM — very easy-peasy.

So — we changed the selfie.jpg reference to a blob ref — pseudo-code you can see in the corner — which is a strong link to the actual blob.

If you know how AT Protocol works — you know it goes through the firehose to the App View. The App View sees this post for the first time — says “I don’t have that blob” — asks the PDS for it — gets it — and then — the App View has an image CDN — it puts the blob in the CDN — compresses it to a standard JPEG — so if you upload an iPhone photo — it doesn’t take 10 minutes to load — it’s snappy.

Once it’s in the CDN — we replace the ref with the CDN URL — and that’s what the App View gives to the client — a cache — serving a nice, viewable version — not the raw content from the PDS — but something the client can actually consume and display.

—

Pause for a little bit.

—

Videos — why can’t we just do that for videos?

The problem — videos consist of a lot of images — they’re very big — and harder to process.

With images — you can process them live — an image takes almost no time. So when the App View sees a blob for the first time — it just sticks it in the CDN — and it works — pretty much.

With videos — processing time scales with video length. A 60-second video — depends on compression — your computer — but it scales — so longer videos — increasingly not viable to do live.

Also — a lot more can go wrong — it’s finicky — sometimes it just errors for strange reasons. If we just sent the video out with the post — and it failed — no feedback mechanism — awkward.

And — the nature of microblogging — you’re always seeing 0-second-old posts. If someone posts a video — and you’re looking at your feed — it’s not going to be ready yet — it’s going to just 404.

So — we decided to do a slightly different architecture.

In this case — the client has a video they want to post. Instead of sticking it straight on the PDS — we send it to the video service — video.bsky.app.

The video service has multiple jobs:

First — moderate the video — we don’t want nasty stuff.

Generate the source file — and act as the CDM.

So — the client sends the video to the video service — first step — it generates a standardized, normalized file — and puts it on the PDS. This is the new source of truth for the video — takes whatever crazy format the client has — turns it into an MP4 at a reasonable bitrate — ensures it stays within PDS upload limits — so most videos on the network are standard — not crazy formats — not uncompressed — just reasonable.

This gets a content ID.

At the same time — it’s also doing a different set of processing steps — turning it into an HLS stream. That’s a format where the video is split into chunks — about 5 seconds — and those chunks are in multiple different quality sets — so the client can dynamically switch qualities — that’s how proper video players work.

That’s in a playlist file — in the CDM — and it shunts all those chunks to the CDM.

When the video service is done — it returns this blob ref to the client. Then the client can make the post — with the confidence that the video already exists — and is already processed. So when it goes through to the App View — the App View can just turn it into the CDN URL — and it’s ready to send to the client — no lag.

—

This comes with a lot of benefits:

Videos are available instantly — and you can be certain they’ll work — a processed video will work — fingers crossed — we ironed out some kinks — but we’re pretty sure it’s going to work — it’s in the CDM.

If it doesn’t work — the error can be given to the client while they’re still in the composer — not after they’ve posted — which is super awkward — like — what do you do? Open the composer — the post already exists — so — errors are easier to handle.

Also — videos on the PDS are reasonable — your PDS isn’t full of enormous iPhone videos that are ridiculously huge.

—

But — this isn’t the only path.

Our protocol — we might not be the only app out there — other clients might be using a different video service.

So — we have this “fallback path” — which is pretty much exactly the same as the image flow. The video service watches the firehose — if it sees a video blob it hasn’t seen before — it kicks off that processing job — as if it was just uploaded — like the image flow.

This has downsides — errors get swallowed — it’s just going to 404 until it’s finished — and the source video on the PDS — who knows what it looks like — probably huge — but it ensures that everyone — all participants — have the source of truth on the PDS.

So — this fallback path — shows we’re being responsible on the protocol. The “happy path” — the optimized one — is for meeting user expectations — but the fallback — is for protocol integrity.

—

Some weeks later — I had Jazz — who built the video player — come and talk to me separately — because that was not easy.

But this is more of a protocol talk — I asked Jazz for the latest stats — and apparently — we’ve served 5,000 years of video to date.

That’s 69 human lifespans — apparently.

What was happening 5,000 years ago? The first dynasty of Egypt — we hadn’t added the rocks to Stonehenge yet — it was still a ditch — they were inventing written language — and the world’s first dam was being built.

So — yeah — that’s a long time.

We have some frankly depressing graphs of how many lifespans we’ve served — Jazz is awesome — by the way — they did a tremendous job building this insane processing architecture that can handle the enormous load of the network.

Shout out to Jazz — Devin — and everyone else on the team — of course.

—

That’s all.

The videos from AHOY! European Social Web Day held in Hamburg, Germany, are being republished along with transcripts as part of the process of preparing for ATmosphereConf 2026, taking place March 26th - 29th in Vancouver, Canada.