embetty: embedding tweets and videos through your own server
🐙 Embetty displays remote content like tweets or YouTube videos without compromising your privacy.
At a glance
- What is it?
- embetty is a MIT licensed TypeScript component that renders remote tweets, Mastodon statuses and videos by fetching them through a server you host, so the reader's browser never talks to the source platform directly. The catch is stated plainly in the caveats: the server does not proxy video data, so tracking protection stops working once a play button is clicked.
- Who is it for?
- embetty is a good fit for a publication or blog that embeds social posts and wants to stop leaking its readers' IP addresses and cookies to those platforms, because the custom element approach means one script tag and one meta tag and no per-site configuration, and the MIT licence keeps it easy to self-host; it is the wrong fit if you need video tracking protection to survive playback, since the server does not proxy video data, and before you deploy you should plan for the server component on your own infrastructure, because the client is useless without it.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 169 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The client is useless without a server you run
embetty displays remote content such as tweets or videos without compromising your privacy, and the mechanism is a split into two pieces that you have to understand before anything else.
The first is the client, a JavaScript component that lives in your page. The second is a server component that you run on your own infrastructure, in a separate repository, and that does the actual fetching. The client does not call Twitter, YouTube or Mastodon. The server does, and hands back rendered content.
That is the whole privacy argument. When a reader loads a page with an ordinary third-party embed, their browser contacts the platform directly, and the platform sees the reader's IP address, their cookies, their user agent, and their referrer. With embetty, the reader's browser contacts your server, and your server contacts the platform. The platform sees your server, not your readers.
The setup sequence is three steps: set up the embetty-server, include the embetty library in your HTML document, and insert an embed with a custom tag. The configuration is a single meta tag pointing at the server:
<head>
<meta data-embetty-server="/path/to/embetty-server" />
</head>The value is a path, so the server can sit on the same origin as your site, behind the same reverse proxy, and the reader never sees a second hostname at all. The demo pages show it in action.
The primary language is TypeScript, the licence is MIT, and the repository is not archived, with the last push on 2026-05-07.
Custom elements instead of a JavaScript API
The embedding syntax is the part of the design that is genuinely pleasant, and it is worth understanding why it was chosen that way.
An embed is an HTML element with a hyphenated name and data attributes:
<embetty-tweet status="1166685910030790662"></embetty-tweet>The alternative would be a JavaScript API where you find a placeholder div, call a function with a URL and a type, and get back a rendered element. The custom element approach means the embed is part of the document, it can be styled and laid out with CSS, and it works with a server-rendered page, because the script tag is loaded with async and the elements are already there when it arrives.
The attributes are specific per type. For a tweet, the status attribute carries the tweet ID, and the include-thread attribute pulls in the conversation above the tweet, described as the context so far. For a video, the type attribute is facebook, vimeo or youtube and the video-id attribute carries the ID.
Two more attributes are worth knowing. The poster-image attribute takes a URL and overwrites the preview image of the video, which is how you keep the thumbnail from being fetched from the platform. And for vimeo and youtube videos, a start-at attribute takes a time in seconds so the video begins at a specific point rather than at the beginning.
Mastodon has its own element, embetty-mastodon, and its status attribute takes the full link to the status rather than an ID, because the server retrieves the data from the corresponding Mastodon instance.
Three ways to get embetty.js onto your site
There are three options for including the client, and the difference between them is about who runs the build.
The first is to download an Embetty archive from the releases page and make embetty.js available on your site. Nothing else is needed, because the client is a plain script that talks to your server.
The second is to use Embetty in an npm project, installing the component package and importing it into your main script:
yarn add @embetty/component
npm install @embetty/component --saveThe import is side-effecting, import '@embetty/component', which is what registers the custom elements. That is the shape a bundler-based site wants, where the script is hashed and cached like any other asset.
The third is to clone the repository and build the bundle yourself:
git clone https://github.com/heiseonline/embetty
cd embetty
yarn
yarn buildThat produces ./dist/embetty.js, which you can then serve like the archive from the first option.
The build path is a little more involved than it looks, because the repository is a monorepo. The root package is named @embetty/monorepo, is marked private, and its build script is pnpm -r build, which runs the build in every workspace package. There is a pnpm-workspace.yaml and a pnpm-lock.yaml at the top level, the package manager is pinned to pnpm 9.9.0 with an integrity hash, and the repository carries a .changeset/ directory, so releases are cut with changesets and the version bump is written by changeset version.
So the clone-and-build route is a pnpm workspace build, not a single package build, and the published npm package is assembled from it.
The caveat that decides the privacy claim's limits
This is the most important paragraph in the project, and it is short.
The Embetty server component does not proxy video data. That means the tracking protection becomes ineffective after the play button has been clicked.
The mechanism is visible in the events documentation. There are two events. The initialized event fires when the embed enters the viewport, and the activated event fires when the video embed has been replaced with an iframe containing the original video player, with the content of the iframe possibly still loading.
So the flow is: the video is fetched as a poster image through the server, the reader sees a preview, and when they click play the actual player is inserted as an iframe pointing at the platform. At that moment the platform gets the reader's IP address again, because the iframe loads directly from YouTube, Vimeo or Facebook rather than through your server.
Whether that matters depends on what you are defending against. A reader who is browsing and never presses play never contacts the platform at all, and the site operator's server does not receive their address either. A reader who presses play is exposed in the ordinary way, and no amount of server-side work changes that unless the video data is proxied as well.
The honest framing is that embetty protects passive reading, not playback. The README says so directly rather than letting a reader assume the protection is total, and that candour is the right call for a privacy tool.
Events, the example directory, and the client API surface
The client exposes a small event surface, and the two documented events are enough to build a page that behaves well.
The initialized event fires when an embed enters the viewport, which is the natural hook for a lazy loading pattern: you can do nothing until the embed scrolls into view. The activated event fires when the video embed has been replaced with the iframe, which is the hook for the moment playback becomes visible to the platform.
The example given is a single line:
document.querySelector('embetty-tweet').addEventListener('initialized', function(e) { ... })Because the embeds are custom elements, they are ordinary DOM nodes, so querySelector and addEventListener work as they would on any element. There is no separate event bus to learn and no subscription object to manage.
The repository carries an example/ directory with index.html, test.html, tweet.html and video.html, plus an app.css, which is a working reference for each embed type. There is also a tweets.json file at the top level, which is presumably a fixture of tweet data used by the tests.
The testing instructions are four steps: clone the repository, run yarn, export the TWITTER_ tokens mentioned in the embetty-server repository, then run yarn test. The token dependency is worth noting, because the client tests apparently exercise real fetches rather than a recorded fixture, which is a reasonable choice for a component whose job is to render what a server returns.
The server image, and where the project actually lives
A Dockerfile in the repository shows how the server side is built and run, and it is worth reading for two details.
The build stage starts from node:lts-alpine, enables corepack, installs with a frozen lockfile, builds the workspace, and then runs a filtered deploy for the server package into a .target directory. The runtime stage is a second node:lts-alpine image with the environment set to Europe/Berlin timezone and de_DE.UTF-8 locale, NODE_ENV set to production, PORT set to 8080, and a NODE_OPTIONS value that caps the old space at 500 MB, disables the experimental fetch, and forces IPv4-first DNS result ordering.
That last detail is not arbitrary. The old space cap keeps a container from ballooning, the disabled experimental fetch keeps the runtime on a stable API, and the IPv4-first ordering avoids a class of connectivity problem in containers on hosts with mixed address families.
The security posture is also visible. The runtime stage copies the deployed files with ownership set to nobody, switches to the nobody user, and sets WORKDIR to /app. The image does not run as root, which is the right default for a service that holds API tokens.
The repository structure explains the split. There is an embetty-server/ directory inside the repository as well as the separate server repository the readme links to, an embetty/ directory for the client, libs/ for shared code, tools/ for the build scripts, and a pnpm-workspace.yaml tying them together. So the monorepo contains both halves, while the readme points at a separate embetty-server repository for the standalone server, which suggests the server was split out for its own release cycle and the monorepo keeps a copy for development.
On releases, the npm packages carry the @embetty/server scope, and the most recent ones are 3.2.8, 3.2.7 and 3.2.6, all published on 2024-09-09 within a couple of hours of each other. Those are the published versions rather than GitHub releases, and they are nearly two years older than the last push, which is a gap worth checking against the server repository before you plan a deployment.
Editorial conclusion
embetty is a good fit for a publication or blog that embeds social posts and wants to stop leaking its readers' IP addresses and cookies to those platforms, because the custom element approach means one script tag and one meta tag and no per-site configuration, and the MIT licence keeps it easy to self-host; it is the wrong fit if you need video tracking protection to survive playback, since the server does not proxy video data, and before you deploy you should plan for the server component on your own infrastructure, because the client is useless without it.
Frequently asked questions
How do I add an embetty embed to a page?
Point a meta tag at your embetty-server with data-embetty-server, load the client script, and add the custom element. A tweet uses <embetty-tweet status="1166685910030790662"></embetty-tweet>, and a video uses the type attribute with a value of facebook, vimeo or youtube plus the video-id attribute.
Does embetty protect my readers when a video is played?
No, and the readme says so. The server does not proxy video data, so the tracking protection becomes ineffective after the play button has been clicked, because the activated event replaces the embed with an iframe pointing at the original video player. Passive reading stays protected.
How do I install the embetty client?
Three options: download an archive from the releases page and serve embetty.js, install the npm package with yarn add @embetty/component or npm install @embetty/component --save and import it, or clone the repository and run yarn and yarn build to produce ./dist/embetty.js.
Which events does embetty fire?
The initialized event fires when an embed enters the viewport, and the activated event fires when a video embed has been replaced with an iframe containing the original video player, which may still be loading. Both are ordinary DOM events on the custom element.
Can embetty embed Mastodon posts?
Yes. The embetty-mastodon element takes a status attribute with the link to the status, and embetty retrieves the data from the corresponding Mastodon instance.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/heiseonline-embetty)Was this useful?