matrix embeds: why they are hard to implement and what to do about that
- FUCKING. LOVE. EMBEDS. it’s such a cool part of the internet. look at this.

embeds are a great way to show people deer pictures. and they keep the credit of the artist, so you can go to their website and give them money. on discord, embeds are generated by a preview server. here’s roughly what that looks like:

great! this is really simple! why are we still talking about this
matrix is a protocol. it’s a thing. we love matrix!
matrix has a client (your phone) and a server (your homeserver, it’s where your messages get stored and stuff). the servers talk to each other instead of there just being one server. that’s it basically. you can make your own server it’s fun.
now let’s imagine i send you that deer picture to your phone. you’d imagine the same thing: your phone asks your server what’s up with this link, it goes and does it’s thing with bluesky, sends it back, win. this should work! (this is /preview_url)
however: i’ve encrypted our room. your client doesn’t fetch embeds in encrypted rooms because it’s potentially sensitive data. in theory this stuff should be fine for bluesky links and such, but what if i later send you very secret cake baking recipes (on secretcakebakingrecipes.com of course) for our upcoming party. this data is very sensitive and government agents are spying on us trying to figure out our cake baking recipes so they can ruin our party. all they would have to do is listen to what your server is doing… hate those guys…
the issue is that it’s impossible to make embeds without leaking data somewhere.

-
this is msc4095: bundled url previews
- in theory this is the best one! i’m under 100 layers of vpn and no government agent will stop me from accessing my cake recipes.
- however!!!! it’s still looking bad for our recipe security. instead of just generating the embed right there and then on my phone, some clients actually just call the server again!! oh no!
- also, it would only take one slip on my ultra-secure vpn setup when sending you recipes and i would have a ping directly from my device to the website, which could be bad!
- you can also imagine some form of attack where an evil person sends me an ip grabber, then i have to send it to them for some reason? they’ll know where i pee…
-
YOUR SERVER. this is the normal way that things go.
- it’s possible that someone could listen to your server’s requests and find out what your server is looking at..
- it’s possible that an EVIL SERVER ADMIN could also just listen to your requests. i’m not quite sure if the attack would identify who exactly you are on the server, though
-
YOUR PHONE.
- probably not the best place
- some special stuff might be done here. for example a youtube video has to be iframed for you to see it
- trivially easy to send you an ipgrabber website
so what do we doo. hellp!
it’s looking like the correct way to do things is:
- in encrypted rooms, always use msc4095 (bundled url previews) and never ever talk to the server. unless the client wants to i guess
- in unencrypted rooms, use msc4095, but fallback to talking to the server if there isn’t an embed
there’s a couple issues with this:
- web clients won’t be able to do msc4095. it relies on sending messages to websites directly, which is just not a thing you can do on a website. there are some exceptions to this but because of this evil thing called CORS we can’t do this without introducing another server (and therefore another vulnerability)
- how do we explain this all to the user??
Privacy VS Who Cares
if you’re unfortunate enough to live in the continent of europe you probably know that cookie banners exist. they are pointless and the worst part of the internet. they suffer from one issue: who the fuck would want to accept these??

these are called dark patterns and they make people press buttons they don’t want to press and basically just violate the consent of the user constantly. it’s awful.
these and a lot of other things (e.g. tech-politics at large) causes a lot of people to be tired by the idea of privacy. privacy is a war that many people lose on a daily basis. some of these people might even use our matrix client! so how do we make a design that:
- explains link preview privacy (your ip may be leaked to this website, you might use the internet oh god, you can prevent this)
- gives the user power to be private if they want to while also not overburdening people who don’t care
- give the user who doesn’t care the most privacy we can without being annoying
- don’t overwhelm the user with like 500 options please
the answer is i don’t know okay! it’s an open question. i’ve floated around some ideas before, like adding domain exceptions in encrypted rooms for serverside stuff. (“trust this domain” or whatever) but maybe people don’t even care about this!
the answer is: we can’t know because we don’t have UX/UI funding. we can only guess at the answer and correct for it over time. it’s a bummer ending sorry!
(Aside??) OEmbed/OpenGraph suuucks actually
lemme talk about opengraph first because its more important. opengraph tags go in the head of your html document. let’s look at the deer link, i’m sure it’s got to be simple!
<meta name="theme-color" content="#FFFFFF">
<meta property="og:site_name" content="Bluesky Social">
<meta property="og:logo" content="https://web-cdn.bsky.app/static/favicon.png">
<meta name="twitter:site" content="@bluesky">
<link type="application/activity+json" href=""> <!-- activity streams (scary!) -->
<meta property="og:type" content="article">
<meta property="profile:username" content="jayrnski.bsky.social">
<meta property="og:url" content="https://bsky.app/profile/jayrnski.bsky.social/post/3mx7wzd5mp22h">
<meta property="og:title" content="Jayrnski (@jayrnski.bsky.social)">
<meta name="description" content="a whole lotta maple :3 🍁">
<meta property="og:description" content="a whole lotta maple :3 🍁">
<meta property="twitter:description" content="a whole lotta maple :3 🍁">
<meta property="og:image" content="https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:qfe2ticzu772ge2rzwfbrlz3/bafkreidwdg34zcqzq5v6axh77ocgmwyazqte3ql6dr5wh4576rpzfy6gua">
<meta property="twitter:image" content="https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:qfe2ticzu772ge2rzwfbrlz3/bafkreidwdg34zcqzq5v6axh77ocgmwyazqte3ql6dr5wh4576rpzfy6gua">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:label1" content="Posted At">
<meta name="twitter:value1" content="2026-10-06T17:22:36.956Z">
<meta name="twitter:label2" content="Likes">
<meta name="twitter:value2" content="7231">
<meta name="twitter:label3" content="Replies">
<meta name="twitter:value3" content="84">
<meta name="twitter:label4" content="Reposts">
<meta name="twitter:value4" content="1185">
<meta property="article:published_time" content="2026-10-06T17:22:36.956Z">
<link rel="alternate" type="application/json+oembed" href="https://embed.bsky.app/oembed?format=json&url=at%3A%2F%2Fdid%3Aplc%3Aqfe2ticzu772ge2rzwfbrlz3%2Fapp.bsky.feed.post%2F3mx7wzd5mp22h">
<link rel="alternate" href="at://did:plc:qfe2ticzu772ge2rzwfbrlz3/app.bsky.feed.post/3mx7wzd5mp22h">so that’s a mess. (twitter??) there’s an oembed in there. maybe that’s better?
{
author_name: "Jayrnski (@jayrnski.bsky.social)",
author_url: "https://bsky.app/profile/jayrnski.bsky.social",
cache_age: 86400,
html: '<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:qfe2ticzu772ge2rzwfbrlz3/app.bsky.feed.post/3mx7wzd5mp22h" data-bluesky-cid="bafyreickoe4w737bxd4f4bqietcyayqadefpn5sjdwzcjzftzk5ezyfsey"><p lang="en">a whole lotta maple :3 🍁</p>— <a href="https://bsky.app/profile/did:plc:qfe2ticzu772ge2rzwfbrlz3?ref_src=embed">Jayrnski (@jayrnski.bsky.social)</a> <a href="https://bsky.app/profile/did:plc:qfe2ticzu772ge2rzwfbrlz3/post/3mx7wzd5mp22h?ref_src=embed">2026-10-06T17:22:34.847Z</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>',
provider_name: "Bluesky Social",
provider_url: "https://bsky.app",
type: "rich",
version: "1.0",
width: 600,
height: /* it said null here but i can't include that word because it breaks my ssr lol */
}WHAT. it’s just html??? with an inline script tag??? why can’t you just give me some structured information that makes sense??
the issue with all this stuff is that there’s no way to uhhh. do this nicely. there’s nothing i can do as a developer to support every website perfectly. don’t even get me started on multiple images (like a gallery on bluesky) or the tumblr neue post format. it’s all a mess and i don’t wanna do this.
for bluesky specifically we are probably just going to do client side embeds, because they have a nice decentralised api for this.
information section of awesomeness: i work on Sable in my free time, i’m a contributor somehow deemed important enough to be allowed to push to main. i personally quite like Sable, and i hope you do too, but you don’t need to use it. i’m not selling you on Sable.
Last updated: Oct 7, 2026 ()