<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://mackuba.eu</id>
  <title>MacKuba.eu</title>
  <subtitle>Kuba Suder's blog on Mac/iOS &amp; web dev</subtitle>
  <link href="https://mackuba.eu" rel="alternate" type="text/html"/>
  <link href="https://mackuba.eu/feed.xml" rel="self" type="application/atom+xml"/>
  <updated>2026-02-04T19:15:43Z</updated>
  <author>
    <name>Kuba Suder</name>
    <email>jakub.suder@gmail.com</email>
  </author>
  <entry>
    <id>https://mackuba.eu/2026/02/04/pds-undockered/</id>
    <title>Running Bluesky PDS undockered</title>
    <published>2026-02-04T19:14:20Z</published>
    <updated>2026-02-04T19:14:20Z</updated>
    <link href="https://mackuba.eu/2026/02/04/pds-undockered/"/>
    <content type="html">&lt;p&gt;A bit over a year ago, in the first week of January 2025, I&amp;nbsp;migrated my main Bluesky account to my own PDS on a Netcup VPS. It&amp;rsquo;s been quite easy to set up using the official installer, and it&amp;rsquo;s been running pretty much without any problems or maintenance the whole year.&lt;/p&gt;

&lt;p&gt;Despite that, I&amp;nbsp;haven&amp;rsquo;t been 100% happy with this setup for one reason: Docker. So I&amp;nbsp;decided to try to take it out of the box, and I&amp;nbsp;made it run first on the same VPS installed separately, and then moved it to another machine this month with a clean install. This blog post is a guide to how I&amp;nbsp;did this, if you&amp;rsquo;re interested. There are a few existing posts about this already:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://benharri.org/bluesky-pds-without-docker/"&gt;https://benharri.org/bluesky-pds-without-docker/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://char.lt/blog/2024/10/atproto-pds/"&gt;https://char.lt/blog/2024/10/atproto-pds/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cprimozic.net/notes/posts/notes-on-self-hosting-bluesky-pds-alongside-other-services/"&gt;https://cprimozic.net/notes/posts/notes-on-self-hosting-bluesky-pds-alongside-other-services/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;But I&amp;nbsp;figured it doesn&amp;rsquo;t hurt to make another one that does things slightly differently again. &amp;ldquo;There are many like this, but this one is mine&amp;rdquo;. (I&amp;nbsp;mostly followed the &lt;a href="https://benharri.org/bluesky-pds-without-docker/"&gt;benharri.org version&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Note: I&amp;rsquo;m describing what I&amp;nbsp;did to migrate an &lt;em&gt;existing&lt;/em&gt; PDS from in-Docker to outside-Docker, so I&amp;nbsp;already had existing data and &lt;code&gt;pds.env&lt;/code&gt; config; if you wanted to install one from scratch this way, you&amp;rsquo;d probably need to also set up the config manually.&lt;/p&gt;

&lt;p class="image"&gt;
  &lt;video width="480" autoplay preload loop&gt;&lt;source src="https://mackuba.eu/images/posts/why.mp4?1771213129" type="video/mp4"&gt;&lt;/video&gt;
&lt;/p&gt;


&lt;p&gt;You might be asking: why? And that&amp;rsquo;s a good question. I&amp;nbsp;mostly wouldn&amp;rsquo;t recommend this setup over the standard Docker one by default, unless you know what you&amp;rsquo;re doing. The standard installation is literally running one command and answering some questions, and then it auto-updates and manages everything.&lt;/p&gt;

&lt;p&gt;My reason is that I&amp;rsquo;m generally pretty familiar with installing things on Linux servers manually, but I&amp;rsquo;m completely unfamiliar with Docker. I&amp;nbsp;always wanted to do some modifications on the PDS, but I&amp;nbsp;didn&amp;rsquo;t know how, because the Docker setup basically takes over the whole server for itself. I&amp;nbsp;don&amp;rsquo;t know where it pulls code from, I&amp;nbsp;don&amp;rsquo;t know where it puts it, and I&amp;nbsp;don&amp;rsquo;t know when it can overwrite any changes I&amp;nbsp;make. I&amp;nbsp;don&amp;rsquo;t feel in control. (And to be clear, this is likely a me problem.)&lt;/p&gt;

&lt;p&gt;So here&amp;rsquo;s what I&amp;nbsp;did (this setup is for Ubuntu 24.04 Noble):&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Install Nginx&lt;/h2&gt;

&lt;p&gt;The standard PDS distribution uses Caddy, but I&amp;nbsp;use Nginx everywhere and I&amp;nbsp;have configs built for it, so I&amp;rsquo;ve set up Nginx:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;# install Nginx
sudo apt-get install --no-install-recommends nginx-light

# enable HTTP on the firewall
sudo ufw allow http/tcp
sudo ufw allow https/tcp

# if you haven't enabled ufw before:
sudo ufw limit log ssh/tcp
sudo ufw enable
&lt;/pre&gt;

&lt;p&gt;Also here&amp;rsquo;s a standard thing I&amp;nbsp;do on VPSes to let me install webapps in &lt;code&gt;/var/www&lt;/code&gt; from my account:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;# set up environment for webapps

sudo groupadd deploy
sudo adduser psionides deploy
sudo chown root:deploy /var/www
sudo chmod 775 /var/www
&lt;/pre&gt;

&lt;p&gt;I&amp;nbsp;also need Certbot for LetsEncrypt:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;# install Certbot

sudo apt-get install --no-install-recommends certbot python3-certbot-nginx
sudo certbot plugins --nginx --prepare
&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;certbot plugins --nginx --prepare&lt;/code&gt; does some initial setup of some config files in &lt;code&gt;/etc/letsencrypt&lt;/code&gt; that are unrelated to specific certificates.&lt;/p&gt;

&lt;p&gt;One thing to note is that the standard setup with Caddy uses a &lt;code&gt;.well-known&lt;/code&gt; route to verify any handles under &lt;code&gt;*.yourdomain.com&lt;/code&gt;, and it automatically creates HTTPS certificates for those subdomains; this wouldn&amp;rsquo;t be as simple here, but I&amp;nbsp;don&amp;rsquo;t need to be able to mass create new handles under my domain. If I&amp;nbsp;ever need one or two, I&amp;rsquo;ll just set them up manually.&lt;/p&gt;

&lt;h2&gt;Email&lt;/h2&gt;

&lt;p&gt;We&amp;rsquo;ll also need email – you can use an external SMTP, but I&amp;nbsp;like to use local sendmail that&amp;rsquo;s configured to forward emails to my main account:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;# install Postfix
sudo apt-get install --no-install-recommends postfix

  # choose: "Internet site"
  # enter domain: "lab.martianbase.net"

# set up the email forwarding
sudo nano /etc/postfix/virtual

  # add a line like this:
  # kuba@lab.martianbase.net my.real.email@domain.com

sudo nano /etc/postfix/main.cf

  # add:
  # virtual_alias_domains = lab.martianbase.net
  # virtual_alias_maps = hash:/etc/postfix/virtual

sudo postmap /etc/postfix/virtual
sudo service postfix reload

# enable SMTP on firewall
sudo ufw allow smtp/tcp
&lt;/pre&gt;

&lt;p&gt;If you set things up this way, you need to set the &lt;code&gt;PDS_EMAIL_SMTP_URL&lt;/code&gt; in &lt;code&gt;pds.env&lt;/code&gt; to &lt;code&gt;smtp:///?sendmail=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note: you will probably need to set up a few things the right way in the DNS and might also need more tweaks in the &lt;code&gt;/etc/postfix/main.cf&lt;/code&gt; for the email sending and forwarding to work. I&amp;nbsp;honestly don&amp;rsquo;t understand the Postfix config well enough, I&amp;nbsp;just have a setup that works and I&amp;nbsp;don&amp;rsquo;t touch it, but it&amp;rsquo;s old and I&amp;nbsp;don&amp;rsquo;t know how correct it is, so I&amp;nbsp;won&amp;rsquo;t share it here. From my experience, it&amp;rsquo;s generally not super hard to configure self-hosted email in such way that it works for sending emails &lt;em&gt;to you&lt;/em&gt; and only to you (like I&amp;nbsp;have with my PDS and my Mastodon instance). If it goes to spam, you know where to look, and if you move it to the inbox, generally the email service should remember and whitelist the sender. (Sending emails to &lt;em&gt;others&lt;/em&gt; is a whole different story of course.)&lt;/p&gt;

&lt;h2&gt;NodeJS&lt;/h2&gt;

&lt;p&gt;Next, NodeJS. I&amp;nbsp;used &lt;a href="https://asdf-vm.com"&gt;asdf&lt;/a&gt; to install it (the version 0.15 is because in 0.16 they did a complete rewrite in Go from the original Bash version, and some things don&amp;rsquo;t work as before).&lt;/p&gt;

&lt;pre class="brush: bash"&gt;# install asdf
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.15.0
nano .bashrc

  # add:
  # source "$HOME/.asdf/asdf.sh"

# ---
# log out &amp;amp; back in here - this is also needed for the deploy group to take effect
# ---

# install node
asdf plugin add nodejs

NODE_VER=`asdf latest nodejs 22`
asdf install nodejs $NODE_VER
asdf global nodejs $NODE_VER

corepack enable
asdf reshim nodejs
&lt;/pre&gt;

&lt;p&gt;The Docker version runs on 20.x, but that&amp;rsquo;s almost EOL, so I&amp;rsquo;ve upgraded to the latest 22.x. 24.x doesn&amp;rsquo;t work at the moment, I&amp;nbsp;get some ugly errors during installation.&lt;/p&gt;

&lt;h2&gt;Install the PDS&lt;/h2&gt;

&lt;p&gt;Now the actual PDS code. I&amp;rsquo;ve decided to keep the code in &lt;code&gt;/var/www&lt;/code&gt;, fetch it from git and use &lt;code&gt;git pull&lt;/code&gt; for updates, and keep the data separately in &lt;code&gt;/var/lib&lt;/code&gt;.&lt;/p&gt;

&lt;pre class="brush: bash"&gt;cd /var/www
git clone https://github.com/bluesky-social/pds
cd pds/service

pnpm install --production --frozen-lockfile
&lt;/pre&gt;

&lt;h2&gt;Migrate the data&lt;/h2&gt;

&lt;p&gt;Now it&amp;rsquo;s time to copy over the data from the previous setup.&lt;/p&gt;

&lt;p&gt;On the first server I&amp;nbsp;did it like this (remember to also turn off and disable the &lt;em&gt;old&lt;/em&gt; PDS service in Docker):&lt;/p&gt;

&lt;pre class="brush: bash"&gt;sudo rsync -rlpt /pds /var/lib/
sudo chown -R psionides:psionides /var/lib/pds
&lt;/pre&gt;

&lt;p&gt;For the second one, I&amp;nbsp;made a temporary SSH key on the old server:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;ssh-keygen -t ed25519 -f ~/.ssh/migration -N "" -C "pds migration"
&lt;/pre&gt;

&lt;p&gt;Added it to authorized keys on the new one:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;nano ~/.ssh/authorized_keys
&lt;/pre&gt;

&lt;p&gt;Prepared an empty directory:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;sudo mkdir /var/lib/pds
sudo chown psionides:psionides /var/lib/pds
&lt;/pre&gt;

&lt;p&gt;And rsynced the data from the old one to the new one:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;# install if missing on the target server:
sudo apt-get install rsync

rsync -rltv -e "ssh -i ~/.ssh/migration" /var/lib/pds/ newserver:/var/lib/pds/
&lt;/pre&gt;

&lt;p&gt;I&amp;nbsp;tried to sync it from a local machine first, but I&amp;nbsp;realized it would take much longer, and between two servers on the same network it took less than a minute for many GBs.&lt;/p&gt;

&lt;h2&gt;Start the service&lt;/h2&gt;

&lt;p&gt;With the data copied, it&amp;rsquo;s time to finish the installation. First, an HTTPS certificate – for now, I&amp;nbsp;made one using manual DNS registration before switching the DNS records:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;sudo certbot certonly --manual --preferred-challenges dns --key-type ecdsa \
    -m $myemail --agree-tos --no-eff-email -d lab.martianbase.net
&lt;/pre&gt;

&lt;p&gt;The way it works is that it gives me a kind of verification token, and I&amp;nbsp;need to put it in a TXT DNS record at &lt;code&gt;_acme-challenge.lab.martianbase.net&lt;/code&gt; before pressing continue.&lt;/p&gt;

&lt;p&gt;Then, I&amp;nbsp;added a systemd service at &lt;code&gt;/etc/systemd/system/pds.service&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[Unit]
Description=Bluesky PDS
After=network.target

[Service]
Type=simple
User=psionides
WorkingDirectory=/var/www/pds/service
ExecStart=/home/psionides/.asdf/shims/node --enable-source-maps index.js
Restart=on-failure
EnvironmentFile=/var/lib/pds/pds.env
Environment="NODE_ENV=production"
TimeoutSec=15
Restart=on-failure
RestartSec=1
StandardOutput=append:/var/lib/pds/pds.log

[Install]
WantedBy=default.target
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I&amp;nbsp;also told the PDS what port to run on in &lt;code&gt;pds.env&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;PDS_PORT=3000
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And then enabled it like this:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;sudo systemctl daemon-reload
sudo systemctl enable --now pds
&lt;/pre&gt;

&lt;p&gt;Test?&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ curl http://localhost:3000

         __                         __
        /\ \__                     /\ \__
    __  \ \ ,_\  _____   _ __   ___\ \ ,_\   ___
  /'__'\ \ \ \/ /\ '__'\/\''__\/ __'\ \ \/  / __'\
 /\ \L\.\_\ \ \_\ \ \L\ \ \ \//\ \L\ \ \ \_/\ \L\ \
 \ \__/.\_\\ \__\\ \ ,__/\ \_\\ \____/\ \__\ \____/
  \/__/\/_/ \/__/ \ \ \/  \/_/ \/___/  \/__/\/___/
                   \ \_\
                    \/_/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It&amp;rsquo;s working! Now, the Nginx config.&lt;/p&gt;

&lt;h2&gt;Nginx config for the PDS&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;/etc/nginx/sites-available/pds.site&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# this is needed to proxy the relevant HTTP headers for websocket
map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

upstream pds {
  server 127.0.0.1:3000 fail_timeout=0;
}

server {
  server_name lab.martianbase.net;
  listen 80;
  listen [::]:80;  # ipv6

  # redirect any http requests to https
  location / {
    return 301 https://$host$request_uri;
  }

  # except for certbot challenges
  location /.well-known/acme-challenge/ {
    root /var/www/html;
  }
}

server {
  server_name lab.martianbase.net;
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  ssl_certificate /etc/letsencrypt/live/lab.martianbase.net/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/lab.martianbase.net/privkey.pem;

  access_log /var/log/nginx/pds-access.log combined buffer=16k flush=10s;
  error_log /var/log/nginx/pds-error.log;

  client_max_body_size 100M;

  location / {
    include sites-available/proxy.inc;
    proxy_pass http://pds;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In &lt;code&gt;/etc/nginx/sites-available/proxy.inc&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Origin "";
proxy_set_header Proxy "";

proxy_buffering off;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

tcp_nodelay on;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;sudo ln -s /etc/nginx/sites-available/pds.site /etc/nginx/sites-enabled/
sudo nginx -t
sudo service nginx reload
&lt;/pre&gt;

&lt;h2&gt;DNS&lt;/h2&gt;

&lt;p&gt;At this point it was time to update the DNS and wait…&lt;/p&gt;

&lt;p&gt;Once it started working for me, I&amp;nbsp;also had to wait a bit more for the existing relays to notice the IP change and I&amp;nbsp;needed to poke them a few times to reconnect to me again, like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl https://bsky.network/xrpc/com.atproto.sync.requestCrawl \
  --json '{"hostname": "lab.martianbase.net"}'
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Restarting the server with &lt;code&gt;sudo service pds restart&lt;/code&gt; also sends a &lt;code&gt;requestCrawl&lt;/code&gt; at least to &lt;code&gt;bsky.network&lt;/code&gt;. What you want is to see in the &lt;code&gt;pds.log&lt;/code&gt; that after &lt;code&gt;pds has started&lt;/code&gt; it says &lt;code&gt;request to com.atproto.sync.subscribeRepos&lt;/code&gt;. There was a brief moment when I&amp;nbsp;could post something, but after reloading bsky.app I&amp;nbsp;wouldn&amp;rsquo;t see my post, because the AppView hasn&amp;rsquo;t indexed it yet… thankfully, after the relay finally reconnected, it backfilled the missing events.&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;also updated the Certbot certificate again the normal way so I&amp;nbsp;wouldn&amp;rsquo;t forget about it later:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;sudo certbot certonly --webroot --webroot-path /var/www/html \
    --key-type ecdsa -d lab.martianbase.net
&lt;/pre&gt;

&lt;p&gt;(There&amp;rsquo;s also a more automated Nginx verification method, where it adds the required certificate lines to your Nginx configs automatically, but I&amp;nbsp;don&amp;rsquo;t like it because it makes a terrible mess of the configs…)&lt;/p&gt;

&lt;h2&gt;Gatekeeper&lt;/h2&gt;

&lt;p&gt;There was one more thing I&amp;nbsp;did while I&amp;nbsp;was already messing with the PDS, which was to add Bailey Townsend&amp;rsquo;s &lt;a href="https://tangled.org/baileytownsend.dev/pds-gatekeeper"&gt;Gatekeeper service&lt;/a&gt;, which restores support for email-based 2FA. For some reason, the email 2FA was implemented in the &amp;ldquo;entryway&amp;rdquo; PDS &lt;code&gt;bsky.social&lt;/code&gt; and not in the main code, and as of today it still hasn&amp;rsquo;t been added to the self-hosted PDS distribution… so Bailey decided that &amp;ldquo;we can just do things&amp;rdquo; and went and added it himself. You just need to install it separately (it&amp;rsquo;s written in Rust).&lt;/p&gt;

&lt;p&gt;First, we install the 🦀:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;# install Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --profile minimal
# (press enter)

source ~/.cargo/env
&lt;/pre&gt;

&lt;p&gt;I&amp;nbsp;used &lt;a href="https://rustup.rs"&gt;rustup&lt;/a&gt;, because Gatekeeper requires a fairly new version of Rust and the one I&amp;nbsp;had in Ubuntu LTS repo didn&amp;rsquo;t cut it.&lt;/p&gt;

&lt;p&gt;We also need some additional dependencies for building:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;# some general compilers and stuff
sudo apt-get install --no-install-recommends build-essential

# openssl and pkg-config
sudo apt-get install --no-install-recommends libssl-dev pkg-config
&lt;/pre&gt;

&lt;p&gt;Now, time to clone and build the service:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;git clone https://tangled.org/baileytownsend.dev/pds-gatekeeper /var/www/gatekeeper
cd /var/www/gatekeeper/
cargo build --release
sudo cp target/release/pds_gatekeeper /usr/local/bin/
&lt;/pre&gt;

&lt;p&gt;(It takes a bit of time to compile, go make yourself a coffee ☕️)&lt;/p&gt;

&lt;p&gt;The current version is missing support for sendmail email transport, so I&amp;nbsp;had to make some tweaks to make it work (&lt;a href="https://tangled.org/baileytownsend.dev/pds-gatekeeper/pulls/6"&gt;see PR&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;There are a couple of ENV entries we need to add to the &lt;code&gt;pds.env&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;PDS_BASE_URL=http://localhost:3000
GATEKEEPER_PORT=8000
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And again, we need to write a systemd service at &lt;code&gt;/etc/systemd/system/gatekeeper.service&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[Unit]
Description=Bluesky Gatekeeper
After=network.target

[Service]
Type=simple
User=psionides
ExecStart=/usr/local/bin/pds_gatekeeper
Restart=on-failure
Environment="PDS_ENV_LOCATION=/var/lib/pds/pds.env"
TimeoutSec=15
Restart=on-failure
RestartSec=1
StandardOutput=append:/var/lib/pds/gatekeeper.log

[Install]
WantedBy=default.target
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And launch it:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;sudo systemctl daemon-reload
sudo systemctl enable --now gatekeeper
&lt;/pre&gt;

&lt;p&gt;Finally, a few updates to the Nginx config:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;upstream gatekeeper {
  server 127.0.0.1:8000 fail_timeout=0;
}

server {
  ...

  location /xrpc/com.atproto.server.createSession {
    include sites-available/proxy.inc;
    proxy_pass http://gatekeeper;
  }

  location /xrpc/com.atproto.server.getSession {
    include sites-available/proxy.inc;
    proxy_pass http://gatekeeper;
  }

  location /xrpc/com.atproto.server.updateEmail {
    include sites-available/proxy.inc;
    proxy_pass http://gatekeeper;
  }

  location /@atproto/oauth-provider/~api/sign-in {
    include sites-available/proxy.inc;
    proxy_pass http://gatekeeper;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And reload it again:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sudo nginx -t &amp;amp;&amp;amp; sudo service nginx reload
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At this point the PDS should be fully operational, including email 2FA when you try to log in 🎉&lt;/p&gt;

&lt;p&gt;For creating accounts and other admin stuff, you can use the &lt;code&gt;pdsadmin&lt;/code&gt; scripts from the source code dir, but you need to pass a &lt;code&gt;PDS_ENV_FILE&lt;/code&gt; env var with the path to &lt;code&gt;pds.env&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cd /var/www/pds
PDS_ENV_FILE=/var/lib/pds/pds.env bash pdsadmin/account.sh list
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Backups&lt;/h2&gt;

&lt;p&gt;There&amp;rsquo;s one last thing – it would also be nice to have backups of your nearly 3 years worth of Bluesky posts…&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;do it like this:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;# create a backup user
sudo adduser archivist

# add a separate SSH key from local Mac
sudo mkdir /home/archivist/.ssh
echo "ssh-ed25519 ... archivist@martianbase.net" | sudo tee /home/archivist/.ssh/authorized_keys
sudo chown -R archivist:archivist /home/archivist/.ssh
&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;sudo nano /usr/local/sbin/backup_pds&lt;/code&gt;:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;#!/bin/bash

set -e

service pds stop
rsync -rlpt --exclude="pds.log*" /var/lib/pds/ /var/backups/pds
chmod -R g+rX /var/backups/pds
chown -R root:archivist /var/backups/pds
service pds start
&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;sudo chmod a+x /usr/local/sbin/backup_pds&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sudo nano /etc/cron.d/backup_pds&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;10 10 * * *   root   /usr/local/sbin/backup_pds
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And then I&amp;nbsp;have a cron job on the local Mac which runs this backup script:&lt;/p&gt;

&lt;pre class="brush: bash"&gt;#!/bin/bash

SSH_COMMAND="ssh -i ~/.ssh/archivist"

rsync -rltq -e "$SSH_COMMAND" "archivist@lab.martianbase.net:/var/backups/pds/" pds
&lt;/pre&gt;

&lt;p&gt;This way, at one point during the day the PDS data is copied to another folder on the server, shutting down the PDS for a moment to make sure the .sqlite files don&amp;rsquo;t end up corrupted, and then at some point later, my Mac separately rsyncs the copy of the data from that second folder, while the files aren&amp;rsquo;t being actively written to. This obviously doubles the data size requirements, so it wouldn&amp;rsquo;t be feasible for a larger PDS, but it&amp;rsquo;s totally fine on mine.&lt;/p&gt;

&lt;p&gt;And that&amp;rsquo;s it. &lt;em&gt;A bit&lt;/em&gt; more than &lt;code&gt;curl | bash&lt;/code&gt;, but I&amp;nbsp;now control every piece of it and I&amp;nbsp;can change them as I&amp;nbsp;please. The old-school way, as God intended 😎&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://mackuba.eu/2025/11/18/atproto-blog-posts/</id>
    <title>ATProto blog posts collection</title>
    <published>2025-11-18T18:59:04Z</published>
    <updated>2025-11-18T18:59:04Z</updated>
    <link href="https://mackuba.eu/2025/11/18/atproto-blog-posts/"/>
    <content type="html">&lt;p&gt;I&amp;nbsp;come across a lot of blog posts about the AT Protocol and Bluesky technicals – both on Bluesky official blogs and those of the team members, and by independent developers from the community. So many people are blogging now (especially now that &lt;a href="https://leaflet.pub"&gt;Leaflet&lt;/a&gt; got popular in these circles) that I&amp;nbsp;&lt;a href="https://lab.mackuba.eu/3m5tyjv3ssc2p"&gt;started using an RSS reader again&lt;/a&gt; just to keep up with everything.&lt;/p&gt;

&lt;p&gt;These posts are usually shared widely for a day or two, and then kind of forgotten – but a lot of them contain some valuable knowledge that is still relevant much later. Even if someone remembers that something like this has been written, it&amp;rsquo;s not always easy to dig it out from the archive.&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;thought it would be nice to have one place collecting those old and newer blog posts to make them easier to find. So I&amp;nbsp;went through those RSS feeds, my &lt;a href="https://lab.mackuba.eu/3m46zeyme3e6n"&gt;like archives&lt;/a&gt; and other places, and collected everything I&amp;nbsp;could find here in an organized list. I&amp;nbsp;also included the documents from the &lt;a href="https://github.com/bluesky-social/proposals"&gt;&amp;ldquo;Proposals&amp;rdquo; GitHub repo&lt;/a&gt;, and various posts from the &lt;a href="https://github.com/bluesky-social/atproto/discussions"&gt;&amp;ldquo;Discussions&amp;rdquo; section&lt;/a&gt; in the ATProto repo.&lt;/p&gt;

&lt;p&gt;This is a subjective selection – from many blogs I&amp;nbsp;skipped some less relevant posts or only included a couple out of many – so if you&amp;rsquo;re interested, click through to the home page from any post and look for the other posts there.&lt;/p&gt;
&lt;hr /&gt;

&lt;p class="noindent"&gt;Search posts by title: &lt;input type="search" id="blog_post_search"&gt;&lt;/p&gt;


&lt;h2&gt;Bluesky official sources&lt;/h2&gt;

&lt;h4&gt;atproto.com&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://atproto.com/articles/atproto-for-distsys-engineers"&gt;ATProto for distributed systems engineers&lt;/a&gt; (Sep 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atproto.com/articles/atproto-ethos"&gt;Atproto Ethos&lt;/a&gt; (Apr 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;bsky.social/about/blog&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/4-13-2023-moderation"&gt;Composable Moderation&lt;/a&gt; (Apr 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial"&gt;How to verify your Bluesky account&lt;/a&gt; (Apr 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/5-5-2023-federation-architecture"&gt;Federation Architecture Overview&lt;/a&gt; (May 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/02-22-2024-open-social-web"&gt;Bluesky: An Open Social Web&lt;/a&gt; (Feb 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/03-12-2024-stackable-moderation"&gt;Bluesky’s Stackable Approach to Moderation&lt;/a&gt; (Mar 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/05-31-2024-search"&gt;Tips and Tricks for Bluesky Search&lt;/a&gt; (May 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/08-06-2024-board"&gt;Bluesky Welcomes Mike Masnick to Board of Directors&lt;/a&gt; (Aug 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/10-24-2024-series-a"&gt;Bluesky Announces Series A to Grow Network of 13M+ Users&lt;/a&gt; (Oct 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/12-30-2024-year-in-review"&gt;2024 In Review&lt;/a&gt; (Dec 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/10-01-2025-patent-pledge"&gt;Bluesky&amp;rsquo;s Patent Non-Aggression Pledge&lt;/a&gt; (Oct 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;docs.bsky.app&lt;/h4&gt;

&lt;p&gt;&lt;details&gt;
  &lt;summary&gt;Click to expand&lt;/summary&gt;&lt;/p&gt;

&lt;h4&gt;2023&lt;/h4&gt;




&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/block-implementation"&gt;Why are blocks on Bluesky public?&lt;/a&gt; (Jun 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/feature-skyfeed"&gt;Featured Community Project: Skyfeed&lt;/a&gt; (Aug 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/create-post"&gt;Posting via the Bluesky API&lt;/a&gt; (Aug 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/repo-sync-update"&gt;Updates to Repository Sync Semantics&lt;/a&gt; (Aug 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/rate-limits-pds-v3"&gt;Rate Limits, PDS Distribution v3, and More&lt;/a&gt; (Sep 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/bgs-and-did-doc"&gt;Bluesky BGS and DID Document Formatting Changes&lt;/a&gt; (Oct 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/protocol-roadmap"&gt;2023 Protocol Roadmap&lt;/a&gt; (Oct 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/repo-export"&gt;Download and Parse Repository Exports&lt;/a&gt; (Nov 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/feature-bridgyfed"&gt;Featured Community Project: Bridgy Fed&lt;/a&gt; (Dec 2023)&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;2024&lt;/h4&gt;




&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/self-host-federation"&gt;Early Access Federation for Self-Hosters&lt;/a&gt; (Feb 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/atproto-grants"&gt;Announcing AT Protocol Grants&lt;/a&gt; (Mar 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/blueskys-moderation-architecture"&gt;Bluesky’s Moderation Architecture&lt;/a&gt; (Mar 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/atproto-grants-recipients"&gt;Meet the second batch of AT Protocol Grant Recipients&lt;/a&gt; (Apr 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/2024-protocol-roadmap"&gt;2024 Protocol Roadmap&lt;/a&gt; (May 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/label-grants"&gt;Labeling Services Microgrants&lt;/a&gt; (May 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/ts-api-refactor"&gt;Typescript API&amp;nbsp;Package Auth Refactor&lt;/a&gt; (Aug 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/oauth-atproto"&gt;OAuth for AT Protocol&lt;/a&gt; (Sep 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/pinned-posts"&gt;Lexicons, Pinned Posts, and Interoperability&lt;/a&gt; (Oct 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/jetstream"&gt;Introducing Jetstream&lt;/a&gt; (Oct 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/relay-ops"&gt;Relay Operational Updates&lt;/a&gt; (Nov 2024)&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;2025&lt;/h4&gt;




&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/looking-back-2024"&gt;Looking Back At 2024 AT Protocol Development&lt;/a&gt; (Jan 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/2025-protocol-roadmap-spring"&gt;2025 Protocol Roadmap (Spring and Summer)&lt;/a&gt; (Mar 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/relay-sync-updates"&gt;Relay Updates for Sync v1.1&lt;/a&gt; (May 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/account-management"&gt;Network Account Management&lt;/a&gt; (May 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/oauth-improvements"&gt;OAuth Improvements&lt;/a&gt; (Jun 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/plc-directory-org"&gt;Creating an Independent Public Ledger of Credentials (PLC) Directory Organization&lt;/a&gt; (Sep 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/taking-at-to-ietf"&gt;Taking AT to the IETF&lt;/a&gt; (Sep 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/incoming-migration"&gt;Enabling Account Migration Back to Bluesky’s PDS&lt;/a&gt; (Sep 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/protocol-checkin-fall-2025"&gt;Protocol Check-in (Fall 2025)&lt;/a&gt; (Oct 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/contact-import-rfc"&gt;Request For Comments: A secure contact import scheme for social networks&lt;/a&gt; (Nov 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog/introducing-tap"&gt;Introducing Tap: Repository Synchronization Made Simple&lt;/a&gt; (Dec 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;&lt;/details&gt;&lt;/p&gt;

&lt;h4&gt;github.com/bluesky-social/atproto/discussions&lt;/h4&gt;

&lt;p&gt;&lt;details&gt;
  &lt;summary&gt;Click to expand&lt;/summary&gt;&lt;/p&gt;

&lt;h4&gt;2023&lt;/h4&gt;




&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/1156"&gt;brainstorm ideas for Cool Developer Tools&lt;/a&gt; (Feb 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/1410"&gt;Intention to remove repository history&lt;/a&gt; (Jul 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/1510"&gt;Planned Changes to DID Documents (August 2023)&lt;/a&gt; (Aug 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/1632"&gt;DID PLC Rate Limits and Validation&lt;/a&gt; (Sep 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/1711"&gt;Upcoming Disruptive Protocol and Infra Changes&lt;/a&gt; (Oct 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/1832"&gt;Migrating bsky.social to Multiple PDS Instances&lt;/a&gt; (Nov 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/1847"&gt;That which we call a “BGS”, By any other name would smell as sweet&lt;/a&gt; (Nov 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/1910"&gt;Tightening Datetime, Record Key, and TID validation&lt;/a&gt; (Dec 2023)&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;2024&lt;/h4&gt;




&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/2128"&gt;Protocol Tech Debt&lt;/a&gt; (Feb 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/2220"&gt;Summary of Recent Changes&lt;/a&gt; (Feb 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/2293"&gt;March 2024 Protocol Updates&lt;/a&gt; (Mar 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/2350"&gt;What does a PDS implementation entail?&lt;/a&gt; (Mar 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/2415"&gt;What goes in to a Bluesky or atproto SDK?&lt;/a&gt; (Apr 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/2656"&gt;OAuth Roadmap&lt;/a&gt; (Jul 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/2687"&gt;Service auth token iteration – method binding &amp;amp; nonces&lt;/a&gt; (Aug 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/2754"&gt;Brainstorming: atproto Dev Tooling and Experience&lt;/a&gt; (Aug 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/2961"&gt;What does an AppView implementation entail?&lt;/a&gt; (Nov 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/3036"&gt;Relay Operational Updates&lt;/a&gt; (Nov 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/3049"&gt;Call for Developer Projects&lt;/a&gt; (Nov 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/3074"&gt;RFC: Lexicon Resolution&lt;/a&gt; (Nov 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/3175"&gt;Account Lifecycle Best Practices&lt;/a&gt; (Dec 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/3176"&gt;Account Migration Details&lt;/a&gt; (Dec 2024)&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;2025&lt;/h4&gt;




&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/3655"&gt;Proposal: OAuth Scopes&lt;/a&gt; (Mar 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/3928"&gt;Relaxing DID PLC Verification Method Constraints&lt;/a&gt; (Jul 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/3950"&gt;OAuth Client Security in the Atmosphere&lt;/a&gt; (Jul 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/4068"&gt;Adding internal repositories to Bluesky’s workflows&lt;/a&gt; (Jul 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/4118"&gt;Progress on Auth Scopes Implementation (August 2025)&lt;/a&gt; (Aug 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/4245"&gt;Draft Lexicon Style Guide (Lexinomicon)&lt;/a&gt; (Oct 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/4316"&gt;PLC Operational Updates&lt;/a&gt; (Oct 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/4343"&gt;Lexicon Language Corner Cases&lt;/a&gt; (Nov 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/atproto/discussions/4437"&gt;Early Permission Sets&lt;/a&gt; (Dec 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;&lt;/details&gt;&lt;/p&gt;

&lt;h4&gt;github.com/bluesky-social/proposals&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0001-user-lists-replygating-and-thread-moderation"&gt;0001: User Lists, Reply-Gating, and Thread Moderation&lt;/a&gt; (Jun 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0002-labeling-and-moderation-controls"&gt;0002: Labeling and Moderation Controls&lt;/a&gt; (Jun 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0003-hashtags"&gt;0003: Hashtags&lt;/a&gt; (Jun 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0004-oauth"&gt;0004: OAuth 2.0 for the AT Protocol&lt;/a&gt; (Feb 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0005-mod-history"&gt;0005: Ozone Moderation History&lt;/a&gt; (Oct 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0006-sync-iteration"&gt;0006: AT Protocol Sync v1.1&lt;/a&gt; (Feb 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0007-mod-report-routing"&gt;0007: Moderation Report Routing&lt;/a&gt; (Feb 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0008-user-intents"&gt;0008: User Intents for Data Reuse&lt;/a&gt; (Mar 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0009-mod-report-granularity"&gt;0009: Moderation Report Granularity&lt;/a&gt; (May 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0010-client-assertion-backend"&gt;0010: Client assertion backend for browser-based applications&lt;/a&gt; (Jun 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0011-auth-scopes"&gt;0011: Auth Scopes for ATProto&lt;/a&gt; (Jun 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluesky-social/proposals/tree/main/0012-infra-abuse-notices"&gt;0012: Infrastructure Abuse Notices&lt;/a&gt; (Nov 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;hr /&gt;

&lt;h2&gt;Bluesky team&lt;/h2&gt;

&lt;h4&gt;jaygraber.medium.com (Jay Graber, CEO)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jaygraber.medium.com/web3-is-self-certifying-9dad77fd8d81"&gt;Web3 is Self-Certifying&lt;/a&gt; (Dec 2021)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;pfrazee.com (Paul Frazee, CTO)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.pfrazee.com/blog/why-facets"&gt;Why RichText facets in Bluesky&lt;/a&gt; (Jan 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pfrazee.com/blog/why-not-rdf"&gt;Why not RDF in the AT Protocol?&lt;/a&gt; (Jan 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pfrazee.com/blog/why-not-p2p"&gt;Why isn&amp;rsquo;t Bluesky a peer-to-peer network?&lt;/a&gt; (Jan 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pfrazee.com/blog/lexicon-guidance"&gt;Guidance on Authoring Lexicons&lt;/a&gt; (Mar 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pfrazee.com/blog/atmospheric-computing"&gt;Atmospheric Computing&lt;/a&gt; (Dec 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;pfrazee.leaflet.pub (Paul Frazee, CTO)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pfrazee.leaflet.pub/3lyucxaykg22w"&gt;We probably need to rename the AppView&lt;/a&gt; (Sep 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pfrazee.leaflet.pub/3lz4sgu7iec2k"&gt;Update on Protocol Moderation&lt;/a&gt; (Sep 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pfrazee.leaflet.pub/3lzhmtognls2q"&gt;Private data: developing a rubric for success&lt;/a&gt; (Sep 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pfrazee.leaflet.pub/3lzhui2zbxk2b"&gt;Three schemes for shared-private storage&lt;/a&gt; (Sep 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pfrazee.leaflet.pub/3m3dogbx2es2g"&gt;Social platforms are not neutral&lt;/a&gt; (Oct 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pfrazee.leaflet.pub/3m5hwua4sh22v"&gt;The politics of purely client-side apps&lt;/a&gt; (Nov 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;bnewbold.net (Bryan Newbold)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bnewbold.net/2024/atproto_progress/"&gt;Progress on atproto Values and Value Proposition&lt;/a&gt; (Aug 2024)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;whtwnd.com/bnewbold.net (Bryan Newbold)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://whtwnd.com/bnewbold.net/3kwzl7tye6u2y"&gt;Notes on Running a Full-Network atproto Relay&lt;/a&gt; (Jul 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://whtwnd.com/bnewbold.net/3l5ii332pf32u"&gt;Migrating PDS Account with &amp;lsquo;goat&amp;rsquo;&lt;/a&gt; (Oct 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://whtwnd.com/bnewbold.net/3lbvbtqrg5t2t"&gt;Reply on Bluesky and Decentralization&lt;/a&gt; (Nov 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://whtwnd.com/bnewbold.net/3lj7jmt2ct72r"&gt;Registering Identity Recovery Keys via PDS, using goat&lt;/a&gt; (Feb 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://whtwnd.com/bnewbold.net/3lo7a2a4qxg2l"&gt;A Full-Network Relay for $34 a Month&lt;/a&gt; (May 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://whtwnd.com/bnewbold.net/3m2j6ccx2bs2t"&gt;AT Moderation Architecture&lt;/a&gt; (Oct 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;bnewbold.leaflet.pub (Bryan Newbold)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bnewbold.leaflet.pub/3m2x7bilyrc23"&gt;AT Namespaces for Community Spaces&lt;/a&gt; (Oct 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bnewbold.leaflet.pub/3m5jsx7qrws2n"&gt;Record Versioning&lt;/a&gt; (Nov 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bnewbold.leaflet.pub/3m7e3hk57rs2u"&gt;Big Indexing&lt;/a&gt; (Dec 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;dholms.leaflet.pub (Daniel Holms)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dholms.leaflet.pub/3m6zswymcqk2p"&gt;PLC Threat-modeling &amp;amp; Auditability&lt;/a&gt; (Dec 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;jimray-bsky.leaflet.pub (Jim Ray)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jimray-bsky.leaflet.pub/3maenxn7kqc2a"&gt;The Importance of Backfillability&lt;/a&gt; (Dec 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;mozzius.dev (Samuel)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mozzius.dev/post/3l777nhz4h32w"&gt;React Native, and &amp;ldquo;the native feel&amp;rdquo;&lt;/a&gt; (Oct 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mozzius.dev/post/3ljlqmchv2b2a"&gt;ATProto by example part 1: Records and Views&lt;/a&gt; (Mar 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;emilyliu.me (Emily Liu, ex-Bluesky)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://emilyliu.me/blog/comments"&gt;Using Bluesky posts as blog comments&lt;/a&gt; (Nov 2024)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;jazco.dev (Jaz, ex-Bluesky)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jazco.dev/2023/09/28/request-coalescing/"&gt;Solving Thundering Herds with Request Coalescing in Go&lt;/a&gt; (Sep 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jazco.dev/2024/01/10/golang-and-epoll/"&gt;Scaling Go to 192 Cores with Heavy I/O&lt;/a&gt; (Jan 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jazco.dev/2024/04/15/in-memory-graphs/"&gt;Your Data Fits in Memory (GraphD Part 1)&lt;/a&gt; (Apr 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jazco.dev/2024/04/20/roaring-bitmaps/"&gt;An entire Social Network in 1.6GB (GraphD Part 2)&lt;/a&gt; (Apr 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jazco.dev/2024/07/05/hls/"&gt;How HLS Works&lt;/a&gt; (Jul 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jazco.dev/2024/09/24/jetstream/"&gt;Jetstream: Shrinking the AT Proto Firehose by &gt;99%&lt;/a&gt; (Sep 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jazco.dev/2025/02/19/imperfection/"&gt;When Imperfect Systems are Good, Actually: Bluesky&amp;rsquo;s Lossy Timelines&lt;/a&gt; (Feb 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jazco.dev/2025/09/26/interning/"&gt;Turning Billions of Strings into Integers Every Second Without Collisions&lt;/a&gt; (Sep 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;hr /&gt;

&lt;h2&gt;ATProto community&lt;/h2&gt;

&lt;h4&gt;whtwnd.com/futur.blue (Futur)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://whtwnd.com/futur.blue/3lkubavdilf2m"&gt;atproto relay any% speedrun&lt;/a&gt; (Mar 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://whtwnd.com/futur.blue/3ls7sbvpsqc2w"&gt;in and out, quick appview adventure&lt;/a&gt; (Jun 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;overreacted.io (Dan Abramov, ex-Bluesky)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://overreacted.io/open-social/"&gt;Open Social&lt;/a&gt; (Sep 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://overreacted.io/where-its-at/"&gt;Where It&amp;rsquo;s at://&lt;/a&gt; (Oct 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;underreacted.leaflet.pub (Dan Abramov, ex-Bluesky)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://underreacted.leaflet.pub/3m23gqakbqs2j"&gt;we can just do things&lt;/a&gt; (Oct 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;bsky.bad-example.com (Phil)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bsky.bad-example.com/consuming-the-firehose-correctly/"&gt;consuming the jetstream firehose correctly&lt;/a&gt; (Feb 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.bad-example.com/can-atproto-scale-down/"&gt;Can atproto scale down?&lt;/a&gt; (Feb 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;mackuba.eu 🙃&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mackuba.eu/2024/02/21/bluesky-guide/"&gt;A complete guide to Bluesky&lt;/a&gt; (Feb 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mackuba.eu/2025/08/20/introduction-to-atproto/"&gt;Introduction to AT Protocol&lt;/a&gt; (Aug 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;shreyanjain.net (Shreyan)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://shreyanjain.net/2024/07/05/nostr-and-atproto.html"&gt;Nostr and ATProto&lt;/a&gt; (Jul 2024)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;jcsalterego.leaflet.pub (Jerry Chen)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jcsalterego.leaflet.pub/3m3x2oftqbs2v"&gt;We live in a space station&lt;/a&gt; (Oct 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;da.vidbuchanan.co.uk (David Buchanan, @retr0.id)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.da.vidbuchanan.co.uk/blog/hacking-bluesky.html"&gt;Hijacking Bluesky Identities with a Malleable Deputy&lt;/a&gt; (Sep 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.da.vidbuchanan.co.uk/blog/adversarial-pds-migration.html"&gt;Adversarial ATProto PDS Migration&lt;/a&gt; (Jul 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;marvins-guide.leaflet.pub (Bailey Townsend)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://marvins-guide.leaflet.pub/3m5fjxkcans2i"&gt;Host a PDS via a Cloudflare Tunnel&lt;/a&gt; (Jul 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marvins-guide.leaflet.pub/3m5fknwepe22y"&gt;A blob in the bucket&lt;/a&gt; (Aug 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marvins-guide.leaflet.pub/3lyqxqbbqkc2p"&gt;What the hell is the atmosphere anyway&lt;/a&gt; (Sep 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marvins-guide.leaflet.pub/3m4qzoj6ubc2h"&gt;What the hell is a rotation key? &lt;/a&gt; (Nov 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;dame.is (Dame)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dame.is/writing/blogs/how-i-made-an-automated-dynamic-avatar-for-my-bluesky-profile/"&gt;How I&amp;nbsp;made an automated dynamic avatar for my Bluesky profile&lt;/a&gt; (Feb 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dame.is/writing/blogs/creating-a-decentralized-bathroom-at-protocol/"&gt;Creating a decentralized bathroom (powered by the AT Protocol)&lt;/a&gt; (Mar 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dame.is/writing/blogs/a-guestbook-and-welcome-message-for-my-pds/"&gt;A guestbook and welcome message for my atproto PDS&lt;/a&gt; (May 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;steveklabnik.com (Steve Klabnik)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://steveklabnik.com/writing/how-does-bluesky-work/"&gt;How Does BlueSky Work?&lt;/a&gt; (Feb 2024)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;knotbin.leaflet.pub (Roscoe Rubin-Rottenberg)&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://knotbin.leaflet.pub/3lx3uqveyj22f"&gt;Wherever you get your Podcasts&lt;/a&gt; (Aug 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;blog.smokesignal.events&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.smokesignal.events/posts/3lwopvsmtx22a-creating-a-did-method-web-identity-for-atprotocol"&gt;Creating a did-method-web Identity for ATProtocol&lt;/a&gt; (Aug 2025)&lt;/li&gt;
&lt;/ul&gt;


&lt;h4&gt;graysky.app&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://graysky.app/blog/2024-02-05-adding-blog-comments"&gt;Adding comments to this blog&lt;/a&gt; (Feb 2024)&lt;/li&gt;
&lt;/ul&gt;


&lt;hr /&gt;

&lt;h2&gt;Other recommended blogs&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://connectedplaces.online"&gt;Connected Places newsletter&lt;/a&gt; (Laurens Hof)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://connectedplaces.leaflet.pub"&gt;Connected Places Leaflet&lt;/a&gt; (Laurens Hof)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.stream.place"&gt;How Streamplace Works&lt;/a&gt; (Eli Mallon)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.tangled.org"&gt;Tangled engineering&lt;/a&gt; (@tangled.org)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ngerakines.leaflet.pub"&gt;Nick&amp;rsquo;s Blog&lt;/a&gt; (Nick Gerakines)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://retrobailey.leaflet.pub"&gt;Bailey&amp;rsquo;s Weekly Retrospective&lt;/a&gt; (Bailey Townsend)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dame.leaflet.pub"&gt;dame&amp;rsquo;s leaflets&lt;/a&gt; (Dame)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://icy.leaflet.pub"&gt;icy takes&lt;/a&gt; (Anirudh Oppiliappan)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://juliet.leaflet.pub"&gt;Juliet&amp;rsquo;s rambles&lt;/a&gt; (Juliet)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://samuel.leaflet.pub"&gt;mildly at-musing&lt;/a&gt; (Samuel)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://smokesignal.leaflet.pub"&gt;The Smoke Signal blog&lt;/a&gt; (@smokesignal.events)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://matthieu.leaflet.pub"&gt;Matthieu&amp;rsquo;s Leaflet&lt;/a&gt; (Matthieu Sieben)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://augment.leaflet.pub"&gt;augment&lt;/a&gt; (Anuj Ahooja)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.tree.fail"&gt;Tree For You&lt;/a&gt; (Tree)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.anew.social"&gt;A New Social&lt;/a&gt; (Bridgy Fed)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lab.mackuba.eu"&gt;Kuba&amp;rsquo;s Lab Notes&lt;/a&gt; 😉&lt;/li&gt;
&lt;/ul&gt;


&lt;script type="text/javascript"&gt;
  document.getElementById('blog_post_search').addEventListener('input', (e) =&gt; {
    let query = e.target.value.trim().toLowerCase();
    let entries = Array.from(document.querySelectorAll('article ul li'));
    let details = Array.from(document.querySelectorAll('article details'));
    let headers = Array.from(document.querySelectorAll('article h4'));

    function ulHasVisibleEntries(h) {
      return Array.from(h.nextElementSibling.querySelectorAll('li')).some(x =&gt; x.style.display === '');
    }

    if (query.length &gt; 0) {
      entries.forEach(li =&gt; { li.style.display = (li.innerText.toLowerCase().includes(query) ? '' : 'none') });
      headers.forEach(h =&gt; { h.style.display = (ulHasVisibleEntries(h) ? '' : 'none') })
      details.forEach(d =&gt; { d.open = true });
    } else {
      entries.forEach(li =&gt; { li.style.display = '' });
      headers.forEach(h =&gt; { h.style.display = '' })
      details.forEach(d =&gt; { d.open = false });
    }
  });
&lt;/script&gt;

</content>
  </entry>
  <entry>
    <id>https://mackuba.eu/2025/10/15/three-bases-one-app/</id>
    <title>How I ran one Ruby app on three SQL databases for six months</title>
    <published>2025-10-15T19:11:16Z</published>
    <updated>2025-10-15T19:11:16Z</updated>
    <link href="https://mackuba.eu/2025/10/15/three-bases-one-app/"/>
    <content type="html">&lt;p&gt;Since June 2023, I’ve been running a service written in Ruby (Sinatra) that provides several &lt;a href="https://blue.mackuba.eu"&gt;Bluesky custom feeds&lt;/a&gt; (initially built with a feed for the &lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/apple"&gt;iOS/Mac developers community&lt;/a&gt; in mind, later expanded to many other feeds). If you don’t know much about Bluesky feeds, you make them by basically running a server which somehow collects and picks existing posts from Bluesky using some kind of algorithm (chronological or by popularity, based on keyword matching, personal likes, whatever you want), and then exposes a specific API&amp;nbsp;endpoint. The Bluesky AppView (API&amp;nbsp;server) then calls your service passing some request parameters, and your service responds with a list of URIs of posts (which the API&amp;nbsp;server then turns into full post JSON and returns to the client app). This lets you share such feed with anyone on the platform, so they can add it to their app and use it like any built-in feed. (If you&amp;rsquo;re interested, check out my &lt;a href="https://github.com/mackuba/bluesky-feeds-rb"&gt;example feed service project&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;In order to provide such service, in practice you need to connect to the Bluesky “firehose” streaming API&amp;nbsp;which sends you all posts made by anyone on the network, and then save either those which are needed for your algorithm, or save all of them and filter later. I&amp;nbsp;chose the latter, since that lets me retry the matching at any time after I&amp;nbsp;modify the keyword lists and see what would be added after that change (and also some of the feeds I&amp;nbsp;now run require having all posts). I&amp;nbsp;also use the same database/service to generate e.g. the &lt;a href="https://blue.mackuba.eu/stats/"&gt;total daily/weekly stats here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All posts made on Bluesky is much less than all posts on Twitter, of course, but it’s still a lot of posts. At the moment (October 2025), there are around 3.5M posts made on average every day; at the last “all time high” in November 2024, it was around 7.5M per day. A post is up to 300 characters of (Unicode) text, but since I&amp;nbsp;also store the other metadata that’s in the record JSON, like timestamp, reply/quote references, embeds like images and link cards, language tags etc., it adds up to a bit less than 1 KB of storage per post on average.&lt;/p&gt;

&lt;p&gt;In addition to that, the firehose stream (if you use the original CBOR stream from a relay, not &lt;a href="https://github.com/bluesky-social/jetstream"&gt;Jetstream&lt;/a&gt;, which is a JSON-serving proxy) includes a lot of overhead data that you don’t need in a service like that, plus all the other types of events like handle changes, likes, follows, blocks, reposts, and so on. The total input traffic is around 15 Mbit/s average right now in October 2025 (or around 5 TB per month), and it used to be around twice that for a moment last year. (Jetstream sends around an order of magnitude less, especially if you ask it to send filtered data, e.g. only the posts.)&lt;/p&gt;

&lt;p&gt;On disk, the millions of posts per day add up to a few gigabytes per day. Since I&amp;nbsp;was running this on a VPS with a 256 GB disk (&lt;a href="https://www.netcup.com/en/?ref=227926"&gt;Netcup, RS 1000&lt;/a&gt; – reflink), I&amp;nbsp;have a cron job set up to regularly prune all older posts and keep only e.g. last 40 days worth of them (since I&amp;nbsp;don’t really need to keep the older posts forever), so around 200-ish gigabytes total, and around 200 millions of rows in the posts table.&lt;/p&gt;

&lt;p&gt;And until March this year, I&amp;nbsp;was keeping all this data in… SQLite 🫠&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Chapter 1: You&amp;rsquo;re probably wondering how I&amp;nbsp;ended up in this situation&lt;/h2&gt;

&lt;p&gt;I&amp;nbsp;think I&amp;nbsp;had never used SQLite in a web app before this – I&amp;nbsp;normally always used MySQL, since that was commonly used in PHP first and then in Ruby webapps (Rails/Sinatra w/ ActiveRecord). I&amp;nbsp;was used to it, I&amp;nbsp;knew how it worked more or less, and I&amp;nbsp;always thought SQLite was only meant to be used in embedded scenarios like native desktop/mobile apps. But the example &lt;a href="https://github.com/bluesky-social/feed-generator"&gt;feed-generator project in JS&lt;/a&gt; shared by Bluesky used SQLite, and since I&amp;nbsp;started out by porting that to Ruby, I&amp;nbsp;ended up also using SQLite, planning to switch to something else later. But you know how it is, that “later” time never comes – and if it ain’t broke, don’t fix it. SQLite worked surprisingly well for much much longer than I&amp;nbsp;expected, with much more data than I&amp;nbsp;expected, and it turns out it can be absolutely ok for server/webapp database purposes, at least in some scenarios. But eventually I&amp;nbsp;started hitting some limitations.&lt;/p&gt;

&lt;p&gt;The main problem is that SQLite doesn’t allow concurrent write access. This means that many processes can read posts or other data simultanously, but only one process at a time can write to the database. This could be a serious problem in most webapps, but it worked fine in this particular architecture. You see, there’s really one entry point to the system where the posts are saved: the firehose stream consumer process. All posts come from there, and nothing else saves posts, the Sinatra API&amp;nbsp;server only makes queries to what was already saved from the firehose. This is why this has worked for me for as long as it did.&lt;/p&gt;

&lt;p&gt;However… at some point I&amp;nbsp;added a second parallel thread, which separately reads data from the &lt;a href="https://plc.directory"&gt;plc.directory&lt;/a&gt; and saves data about some accounts' handles and assigned PDS servers. There are also cron jobs running scripts and Rake tasks, which sometimes modify data and sometimes take a bit to run (like that older posts cleaner), and I&amp;nbsp;sometimes run Rake tasks manually to e.g. rebuild feeds.&lt;/p&gt;

&lt;p&gt;And this is where I&amp;nbsp;started running into a second related problem: how this concurrent writing is/was handled in ActiveRecord. I&amp;nbsp;don’t know if I&amp;nbsp;can explain this all correctly (see &lt;a href="https://github.com/rails/rails/issues/13908"&gt;this GitHub issue&lt;/a&gt; and &lt;a href="https://stackoverflow.com/a/26150137"&gt;this StackOverflow comment&lt;/a&gt;), but the gist is, ActiveRecord used some mode of data locking in SQLite, which resulted in a flow like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A transaction is started which locks the data for reading.&lt;/li&gt;
&lt;li&gt;Some records are loaded and turned into AR models (through a &lt;code&gt;where&lt;/code&gt;/&lt;code&gt;find_by&lt;/code&gt; call etc.).&lt;/li&gt;
&lt;li&gt;Some updates are made to the model objects.&lt;/li&gt;
&lt;li&gt;When I&amp;nbsp;call &lt;code&gt;save&lt;/code&gt; on the model, AR tries to change the lock mode that would allow it to also make a write.&lt;/li&gt;
&lt;li&gt;But something else has made other writes in the meantime.&lt;/li&gt;
&lt;li&gt;Because of how the transaction/locks were set up, SQLite decides that the data I’m trying to save might have been modified in the meantime, and aborts the whole transaction and throws an error (&lt;code&gt;SQLite3::BusyException: database is locked&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;


&lt;p&gt;Changing timeout settings doesn’t fix the problem, because the exception is thrown immediately, without waiting for the other process to finish. What worked around the problem was a mix of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trying to avoid doing multiple writing operations in parallel at all&lt;/li&gt;
&lt;li&gt;&lt;p&gt;rearranging the code a bit artificially so it opens a transaction and then &lt;em&gt;first&lt;/em&gt; does some kind of write, even a completely pointless one, before doing the reads, which makes it create the right kind of lock from the beginning:&lt;/p&gt;

&lt;pre class="brush: ruby"&gt;def rescan_feed_items(feed)
  ActiveRecord::Base.transaction do
    # lulz
    FeedPost.where(feed_id: -1).update_all(feed_id: -1)

    feed_posts = FeedPost.where(feed_id: feed.feed_id).includes(:post).order('time')
    feed_posts.each do |fp|
      ...
    end
  end
end
&lt;/pre&gt;

&lt;p&gt;or so that it does the write without touching the original model, e.g.:&lt;/p&gt;

&lt;pre class="brush: ruby"&gt;# instead of:
@post.update(thread_id: root.id)

# do:
Post.where(id: @post.id).update_all(thread_id: root.id)
&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;This has mostly let me avoid the problem for a long time, but this meant it kept popping back up sometimes, and I&amp;nbsp;had to write some code sometimes in a way that didn’t logically make sense and was only like that to avoid the exceptions. In particular, with the &lt;a href="https://plc.directory"&gt;plc.directory&lt;/a&gt; thread, I&amp;nbsp;had to make it pass any changes back to the main thread through a queue so they can be saved to the database from there.&lt;/p&gt;

&lt;p&gt;(Ironically, the ActiveRecord folks finally &lt;a href="https://github.com/rails/rails/pull/50371"&gt;fixed this whole problem&lt;/a&gt; in the 8.0 release, just as I&amp;nbsp;finished migrating away from SQLite…)&lt;/p&gt;

&lt;p&gt;A second problem was that I&amp;nbsp;started having some performance issues that I&amp;nbsp;couldn’t find a good solution for – e.g. post write operations were occasionally randomly taking e.g. 1 or 2 seconds to finish instead of milliseconds; and I&amp;nbsp;wanted to optimize the app to be able to potentially save as many posts per second as possible, to prepare for larger traffic in the future.&lt;/p&gt;

&lt;p&gt;When I&amp;nbsp;was asking for advice, everyone was telling me “dude, just switch to Postgres” 😛 But I&amp;nbsp;haven’t really worked with Postgres before other than briefly, and I&amp;nbsp;knew some things were different there, and I&amp;nbsp;wasn’t sure if I&amp;nbsp;want to switch to something unknown rather than what I&amp;nbsp;knew (MySQL).&lt;/p&gt;

&lt;p&gt;And since I&amp;nbsp;have way too much time, no life, and probably a good bit of neurodivergence, I&amp;nbsp;chose the most obvious solution: set up the app on &lt;em&gt;both&lt;/em&gt; MySQL and on Postgres on two separate &amp;amp; identical VPSes, and compare how it works in both versions… 🫣&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Chapter 2: Getting it to run&lt;/h2&gt;

&lt;p&gt;Turns out, SQL databases are a bit different from each other in a lot of aspects, and migrating a webapp from one to the other is a bit more work than just editing the &lt;code&gt;Gemfile&lt;/code&gt; and &lt;code&gt;database.yml&lt;/code&gt; – who would&amp;rsquo;ve guessed…&lt;/p&gt;

&lt;p&gt;Beyond the obvious, here are some things I&amp;nbsp;had to change on the &lt;strong&gt;MySQL branch&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Some column type changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;integer column sizes – in SQLite, &lt;a href="https://www.sqlite.org/datatype3.html"&gt;all numbers are just integers of any size&lt;/a&gt;, so here I&amp;nbsp;changed some to &lt;code&gt;smallint&lt;/code&gt; and some to &lt;code&gt;bigint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;similarly, some &lt;code&gt;string&lt;/code&gt; columns were changed to &lt;code&gt;text&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;for &lt;code&gt;datetime&lt;/code&gt; columns, I’ve set the decimal precision explicitly to 6 digits (this is now the &lt;a href="https://github.com/rails/rails/pull/42297"&gt;default since AR 7.0&lt;/a&gt;, I&amp;nbsp;started on 6.x for some reason)&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;In queries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I&amp;nbsp;removed some index hacks like &lt;code&gt;.where("+thread_id IS NULL”)&lt;/code&gt;, which tell the SQLite query optimizer to use/not use a given index&lt;/li&gt;
&lt;li&gt;some date operations had to be rewritten to use different functions, e.g. &lt;code&gt;DATETIME('now', '-7 days')&lt;/code&gt; to &lt;code&gt;SUBDATE(CURRENT_TIMESTAMP, 7)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;some queries had to be rewritten or updated because they were just throwing SQL syntax errors – e.g. I&amp;nbsp;had to explicitly list table names on some fields in &lt;code&gt;SELECT&lt;/code&gt;; or there was this thing in MySQL where I&amp;nbsp;had to &lt;a href="https://stackoverflow.com/a/9843719"&gt;nest a subquery for DELETE one level deeper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ActiveRecord::Base.connection.execute&lt;/code&gt; returns rows as arrays instead of hashes for some reason, indexed by &lt;code&gt;[0]&lt;/code&gt; not by &lt;code&gt;['field']&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;For &lt;strong&gt;Postgres&lt;/strong&gt;, in addition to most of the above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I&amp;nbsp;had to replace &lt;code&gt;0&lt;/code&gt; / &lt;code&gt;1&lt;/code&gt; used as false/true in boolean columns with an explicit &lt;code&gt;FALSE&lt;/code&gt; / &lt;code&gt;TRUE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;date functions in queries were slightly different again, e.g. &lt;code&gt;DATE_SUBTRACT(CURRENT_TIMESTAMP, INTERVAL '7 days')&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;strings in queries had to be changed to all be in single quotes, not in double quotes, which in Pg are reserved for field names like &lt;code&gt;"post_id"&lt;/code&gt; (what SQLite and MySQL use the backticks for: &lt;code&gt;`post_id`&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;I&amp;nbsp;could also finally remove all the code hacks added to work around the SQLite concurrency issues – start normally saving handles in the PLC importer thread, rewrite some transaction blocks to a more logical form, and so on.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Chapter 3: Migrating the data&lt;/h2&gt;

&lt;p&gt;For migration to MySQL, I&amp;nbsp;used the &lt;a href="https://github.com/techouse/sqlite3-to-mysql"&gt;sqlite3mysql&lt;/a&gt; tool, written in Python:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;pip install sqlite3-to-mysql

sqlite3mysql -K -E -f bluesky.sqlite3 -d bluefeeds_production -u kuba -i DEFAULT -t feed_posts handles post_stats subscriptions unknown_records ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For the posts table (which is the vast majority of the database size), I&amp;nbsp;used the &lt;code&gt;-c&lt;/code&gt; (&lt;code&gt;--chunk&lt;/code&gt;) option to import posts in batches:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sqlite3mysql -K -E -f bluesky.sqlite3 -d bluefeeds_production -u kuba -i DEFAULT -t posts -c 1000000
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For Postgres, I&amp;nbsp;used &lt;a href="https://pgloader.readthedocs.io"&gt;pgloader&lt;/a&gt;. Unlike sqlite3mysql, it isn’t configured through command-line flags, but instead you need to write “command” files with a special DSL and then pass the filename in the argument. So my command files looked something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;load database
from sqlite://./db/bluesky.sqlite3
into postgresql:///bluefeeds_production

with data only, truncate
including only table names like 'feed_posts', 'handles', 'post_stats', 'subscriptions', 'unknown_records';
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I’ve split the tables into several command files, because I&amp;nbsp;wanted to do it a bit more step by step since some of the imports were failing.&lt;/p&gt;

&lt;p&gt;For the posts table, I&amp;rsquo;ve similarly set a “prefetch rows” flag to do it in batches:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;load database
from sqlite://./db/bluesky.sqlite3
into postgresql:///bluefeeds_production

with data only, truncate, prefetch rows = 10000
including only table names like 'posts';
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Using the two importers are two very different experiences: sqlite3mysql takes quite a lot of time to import, but shows very nice progress bars and remaining time estimates; pgloader gives you basically no updates until it&amp;rsquo;s finished, and even if some tables fail to import, it&amp;rsquo;s not immediately clear from the summary what happened – however, it does the job in as much as two orders of magnitude less time 😅 I&amp;nbsp;don&amp;rsquo;t know how much this is because of the tool or the database though (and there are probably ways to speed up the import in MySQL, e.g. by dropping indexes first).&lt;/p&gt;

&lt;p&gt;One surprise realization I&amp;nbsp;had during the import: in SQLite, when you define a column as string with limit 50, it doesn’t actually enforce that limit! Apparently I&amp;nbsp;had a whole bunch of records in some tables where the values were much longer than the expected max length… because I&amp;nbsp;was missing Ruby-side AR validations (&lt;code&gt;validates_length_of&lt;/code&gt;) in some models – and those records were being rejected by both new databases. So I&amp;nbsp;had to add all those missing length validations and clean up the invalid data first.&lt;/p&gt;

&lt;p&gt;An additional problem cropped up in MySQL, which has different &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/charset-collation-names.html"&gt;text collation rules&lt;/a&gt; depending on accents and unicode normalization than SQLite &amp;amp; Postgres. I&amp;nbsp;have a “hashtags” table listing all hashtags that appeared anywhere in the posts, with a unique index on the hashtag name – but the import to MySQL was failing, because some hashtags were considered by MySQL as having the same text as some others, while SQLite had considered them different… I&amp;nbsp;tried to pick a different collation for the table (&lt;code&gt;utf8mb4_0900_as_cs&lt;/code&gt;, i.e. both accent-sensitive
and case-sensitive), but that only partially helped with some name pairs (&amp;ldquo;pokemon&amp;rdquo; vs. &amp;ldquo;pokémon&amp;rdquo;), but not with others (different normalization, or invisible control characters, and there are *countless* different types of those, as I&amp;nbsp;have learned…). I&amp;nbsp;eventually gave up and ended up just dropping the unique index for now.&lt;/p&gt;

&lt;p&gt;In Postgres, in turn, the problem was that it apparently doesn’t support strings that contain null bytes, and there are some occasional posts that somehow end up with a &lt;code&gt;\0&lt;/code&gt; in the post text… So I&amp;nbsp;had to just filter out such posts as invalid.&lt;/p&gt;

&lt;pre class="brush: ruby"&gt;if text.include?("\u0000")
  return
end
&lt;/pre&gt;

&lt;p&gt;Finally, something that somehow caused an issue in both versions was a thing that every programmer loves – timezones… When I&amp;nbsp;made a query to count posts added in the last 5 minutes, and it always returned 0, but “last 65 minutes” didn’t, I&amp;nbsp;immediately knew what was going on 🫠&lt;/p&gt;

&lt;p&gt;In Postgres, the solution was to tell ActiveRecord to use the &lt;code&gt;timestamptz&lt;/code&gt; data type for timestamp columns instead of the default &lt;code&gt;timestamp&lt;/code&gt;:&lt;/p&gt;

&lt;pre class="brush: ruby"&gt;ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz
&lt;/pre&gt;

&lt;p&gt;(and then migrate the existing columns to use that type).&lt;/p&gt;

&lt;p&gt;In MySQL, you need to either tell AR to use the timezone the database is using, if it’s not set to UTC:&lt;/p&gt;

&lt;pre class="brush: ruby"&gt;ActiveRecord.default_timezone = :local
&lt;/pre&gt;

&lt;p&gt;Or tell the database to use UTC:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[mysqld]
default-time-zone = '+00:00'
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;(but not both…)&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Chapter 4: Optimizing&lt;/h2&gt;

&lt;p&gt;To be able to test both databases with real production traffic in a way that would let me compare them in a fair competition, I&amp;rsquo;ve set up a kind of “database A/B test” 🙃&lt;/p&gt;

&lt;p&gt;The feed was configured to load data from my original SQLite server as before, but in the request handler on that server, instead of calling the feed class locally, the code picked one of the two other servers based on either the &amp;ldquo;&lt;a href="/2025/08/20/introduction-to-atproto/#identity"&gt;DID&lt;/a&gt;&amp;rdquo; (user ID) of the caller or current timestamp, proxied the call to that server, took the response, and returned it back to the caller:&lt;/p&gt;

&lt;pre class="brush: ruby"&gt;get '/xrpc/app.bsky.feed.getFeedSkeleton' do
  feed_key = params[:feed].split('/').last

  if ['hashtag', 'follows-replies'].include?(feed_key)
    server = Time.now.hour.odd? ? SERVER_A : SERVER_B
  elsif ['replies'].include?(feed_key)
    server = Time.now.hour.even? ? SERVER_A : SERVER_B
  else
    did = parse_did_from_token
    server = did.nil? || did =~ /[a-m0-4]$/ ? SERVER_A : SERVER_B
  end

  url = "https://#{server}/xrpc/app.bsky.feed.getFeedSkeleton?" + request.query_string
  headers = env['HTTP_AUTHORIZATION'] ? { 'Authorization' =&amp;gt; env['HTTP_AUTHORIZATION'] } : {}
  response = Net::HTTP.get_response(URI(url), headers)

  content_type :json
  [response.code.to_i, response.body]
end
&lt;/pre&gt;

&lt;p&gt;This meant that each feed load took a bit longer, but it wasn’t very noticeable in practice, and I&amp;nbsp;had the real traffic split into two more  or less equal parts, going to the two servers. (Coincidentally, it&amp;rsquo;s exactly one year today since I&amp;nbsp;deployed that change – I&amp;rsquo;ve been procrastinating way too long on this blog post 🫠)&lt;/p&gt;

&lt;p&gt;And then started my months-long work of optimizing the databases and queries…&lt;/p&gt;

&lt;p&gt;Some problems showed up immediately: a query was returning data immediately in SQLite, and in MySQL or Postgres it just hangs. So in those cases, I&amp;nbsp;often had to add some missing index, or modify the index somehow (e.g. add an additional field, or switch a field in a composite index to &lt;code&gt;DESC&lt;/code&gt;), or rearrange a query to make it use the intended index, add some limiting condition, or occasionally (in MySQL) add &lt;code&gt;FORCE INDEX&lt;/code&gt;. Those issues were generally fairly easy to fix, and the fix either clearly worked or not. I&amp;nbsp;think some queries I&amp;nbsp;had were just logically not fully thought through, but they had been working fine before because SQLite has some things organized differently on disk and some access patterns work better, hiding the issue with the query.&lt;/p&gt;

&lt;p&gt;The bigger problem and one I’ve spent a ton of time on (in the Postgres version) was one specific query in a set of “replies feeds”. I&amp;nbsp;mostly wrote everything about in on my Journal blog on micro.blog back in January, and I&amp;nbsp;remembered much more about it then than I&amp;nbsp;do now, so I’ll just link to &lt;a href="https://journal.mackuba.eu/2025/01/16/postgress-progress/"&gt;that old blog post here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The TLDR is that I&amp;nbsp;have a set of three feeds: &lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/follows-replies"&gt;Follows &amp;amp; Replies&lt;/a&gt;, &lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/replies"&gt;Only Replies&lt;/a&gt; and &lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/only-posts"&gt;Only Posts&lt;/a&gt;, which share the same code, just with slightly different filters; these are personalized feeds (i.e. having different content depending on who&amp;rsquo;s loading them), which for a given user fetch the list of the accounts that user is following, and then make a query asking the database for &amp;ldquo;most recent N posts from any of the users that this account follows&amp;rdquo; – so basically a reimplementation of the standard &amp;ldquo;Following&amp;rdquo; feed, with some changes.&lt;/p&gt;

&lt;p&gt;This query was much slower on the Postgres server than on the other two databases, and Postgres insisted on using the posts index on &lt;code&gt;(time)&lt;/code&gt; (scanning possibly millions of rows to find the right ones) instead of using the one on &lt;code&gt;(user, time)&lt;/code&gt; some number of times and merging the results (and apparently asking &amp;ldquo;how to do FORCE INDEX in Postgres&amp;rdquo; is a terrible heresy 😛). I&amp;nbsp;spent a lot of time on this and it took me a lot of trial and error, asking more experienced people on Bluesky, reading docs and articles, chatting with ChatGPT, and so on. I&amp;nbsp;went through: bumping up the &lt;code&gt;STATISTICS&lt;/code&gt; target and/or hardcoding &lt;code&gt;n_distinct&lt;/code&gt; (and rolling those back), tweaking some configuration variables, rearranging the query/index in various ways – and what I&amp;nbsp;finally settled on was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;changing the &lt;code&gt;(user, time)&lt;/code&gt; index to also include the &lt;code&gt;id&lt;/code&gt; primary key as the third field, i.e. &lt;code&gt;(user, time DESC, id)&lt;/code&gt;, to let it do &amp;ldquo;Index Only Scans&amp;rdquo; on it – I&amp;nbsp;haven&amp;rsquo;t realized that indexes in Postgres don&amp;rsquo;t reference the primary key, so they can&amp;rsquo;t be used this way unless the &lt;code&gt;id&lt;/code&gt; is explicitly included there!&lt;/li&gt;
&lt;li&gt;and setting up a cron job to do very frequent manual VACUUM (4× a day, with forced &lt;code&gt;index_cleanup&lt;/code&gt;), because otherwise it has to re-check some of the ids fetched from the index to verify if the rows haven&amp;rsquo;t been deleted&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;After those changes, it finally started working really nicely on Postgres, with the mean response time from this query going below 20 ms, while the MySQL version was doing around 50 ms, and the initial Postgres version before the index changes had slowed down to as much as 200-300 ms mean time. (It still occasionally picks the wrong index, for accounts that are somewhere in the middle follows range, over 1000 followed accounts – for those with many thousands, the &lt;code&gt;(time)&lt;/code&gt; index is almost always better – but I&amp;nbsp;think that&amp;rsquo;s somewhat unavoidable.)&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Epilogue: There can be only one&lt;/h2&gt;

&lt;p&gt;In the end, after all the tweaks and optimizations, both servers on both databases were working quite fine, and I&amp;nbsp;think I&amp;nbsp;would probably be ok with either of them. But I&amp;nbsp;had to pick one.&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;ended up picking…&lt;/p&gt;

&lt;p&gt;…&lt;/p&gt;

&lt;p&gt;…&lt;/p&gt;

&lt;p&gt;Postgres! 🏆&lt;/p&gt;

&lt;p&gt;In those few months, I&amp;nbsp;managed to read sooo many pages of the documentation, articles about various specific settings, spent so much time in the &lt;code&gt;psql&lt;/code&gt; console, reading output from the analyzer, that I&amp;nbsp;got much more comfortable with it than I&amp;nbsp;was at the beginning… So ironically, the fact that I&amp;nbsp;had to spend more time tweaking it to get it to work all smoothly made me prefer it in the end. I&amp;nbsp;felt like specifically because there were so many different dials and switches, I&amp;nbsp;felt more &amp;ldquo;in control&amp;rdquo; with Postgres than with MySQL – the tutorials for tuning Postgres mentioned 5-10 different settings at least, and the ones for MySQL basically said “ah, just set &lt;code&gt;innodb_buffer_pool_size&lt;/code&gt; to half the RAM and you’re done”. And if you’ve already set that and you’d like to optimize things further? Well… ¯\_(ツ)_/¯&lt;/p&gt;

&lt;p&gt;Postgres&amp;rsquo;s query analyzer output is also more readable and helps you more with figuring out how it&amp;rsquo;s actually handling the query, and generally various debugging commands seem to provide more readable info about current parameters of the system – while MySQL mostly has a &lt;code&gt;SHOW ENGINE INNODB STATUS&lt;/code&gt; command, which just pukes several pages of text output at you.&lt;/p&gt;

&lt;p&gt;I’ve been doing various manual benchmarks of how different parts of the system work, what the average response times and query times are and so on, on both versions, and keeping the results in tables, but I&amp;nbsp;can’t really get any general conclusion from this on which database is better at what. It was often things like: one does single row deletes faster and the other does multi-row deletes faster, or one is faster at inserts and the other at counts… but generally it was changing a bit too much over time, and I&amp;nbsp;wasn’t doing it in a super scientifically controlled way.&lt;/p&gt;

&lt;p&gt;One thing that I&amp;nbsp;could see on &lt;a href="https://munin-monitoring.org"&gt;Munin charts&lt;/a&gt; in the end was that the Postgres server had higher numbers on the disk &lt;strong&gt;read&lt;/strong&gt; IO/throughput charts, while the MySQL server had noticeably higher numbers on the disk &lt;strong&gt;write&lt;/strong&gt; IO/throughput charts. Not sure if this is a good assumption, but my guess was that with higher traffic in the future, it would generally be easier to scale the read load in Postgres (with various caches, replicas etc.) than to scale the write load in MySQL. Also, in the end after all the optimizations, the key query in the “replies” feeds was working noticeably better in the Postgres version, and in the test “how quickly it can possibly process events when catching up at max speed” (where post record inserts are generally the bottleneck), the Postgres version also ended up with a slightly higher processing speed.&lt;/p&gt;

&lt;p&gt;So since March, the app has been running on a new 512 GB VPS on a Postgres database, and it&amp;rsquo;s been working fine since then.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://mackuba.eu/2025/08/20/introduction-to-atproto/</id>
    <title>Introduction to AT Protocol</title>
    <published>2025-08-20T17:32:45Z</published>
    <updated>2025-08-20T17:32:45Z</updated>
    <link href="https://mackuba.eu/2025/08/20/introduction-to-atproto/"/>
    <content type="html">

&lt;div class="hide-in-intro"&gt;
  &lt;p&gt;&lt;i&gt;(Last update: &lt;a href="#changelog"&gt;15 Oct 2025&lt;/a&gt;.)&lt;/i&gt;&lt;/p&gt;
&lt;/div&gt;


&lt;p&gt;Some time ago I&amp;nbsp;wrote a long blog post I&amp;nbsp;called “&lt;a href="/2024/02/21/bluesky-guide/"&gt;Complete guide to Bluesky&lt;/a&gt;”, which explains how all the user-facing features of Bluesky work and various tips and tricks. This one is meant to be a bit like a developer version of that – I&amp;nbsp;want to explain in hopefully understandable language what all the pieces of the network architecture are and how they all fit together. I&amp;nbsp;hope this will let you understand better how Bluesky and the underlying protocol works, and how it differs from e.g. the Fediverse. This should also be a good starting point if you want to start building some apps or tools on ATProto.&lt;/p&gt;

&lt;p&gt;This post is a first part of a series – next I&amp;nbsp;want to look at some comparisons with the Fediverse and some common misconceptions that people have, and look at the state of decentralization of this network, but that was way too much for one post; so this one focuses on the “ATProto intro tutorial” part.&lt;/p&gt;

&lt;p&gt;But before we start, a little philosophical aside:&lt;/p&gt;
&lt;p id="what-is-bluesky"&gt;&lt;/p&gt;

&lt;h2&gt;What is “Bluesky”? Which “Bluesky” are we talking about?&lt;/h2&gt;

&lt;p&gt;Discussions about Bluesky sometimes get a little confusing because… “Bluesky” could mean a few different things. Language is hard.&lt;/p&gt;

&lt;p&gt;First, we have Bluesky the company, the team. Usually, when people want to clarify that they’re talking about the group of people or the organization, they say “Bluesky PBC” (PBC = Public Benefit Corporation), or “Bluesky team”.&lt;/p&gt;

&lt;p&gt;(If you want to read a bit about where Bluesky came from and what’s the current state of the company, read &lt;a href="/2024/02/21/bluesky-guide/#what-is-bluesky"&gt;these two sections&lt;/a&gt; in the Bluesky Guide blog post.)&lt;/p&gt;

&lt;p&gt;And we also have Bluesky the product, the social network, the thing that they’ve built. This network is not a single black box like Twitter or Facebook are (despite what they say about it on Mastodon), it&amp;rsquo;s more like a set of separate and actually very transparent boxes.&lt;/p&gt;

&lt;p&gt;The system they&amp;rsquo;ve built, of which Bluesky was initially meant to be just a tech demo, is called the &lt;strong&gt;Authenticated Transfer Protocol&lt;/strong&gt;, or AT Protocol, or ATProto. Bluesky is built on ATProto, and it is in practice a huge part of what ATProto currently is, which makes the boundary between Bluesky and non-Bluesky a bit hard to define at times, but it’s still only a subset of it.&lt;/p&gt;

&lt;p&gt;Bluesky in this second meaning is some nebulous thing that consists of: the data types (“&lt;a href="#lexicons"&gt;lexicons&lt;/a&gt;”) that are specific to the Bluesky microblogging aspect of ATProto, like Bluesky posts or follows; the APIs for handling them and for accessing other Bluesky-specific features; the rules according to which they all work together; and the whole “social layer” that is created out of all of this, the virtual “place” – the thing that people have in mind when they say “this website”, even when it’s accessed through a mobile app. One of the coolest things about Bluesky &amp;amp; ATProto, in my opinion, is that it connects many different independent pieces into something that still feels like one shared virtual space.&lt;/p&gt;

&lt;p&gt;People outside the company can create (and are creating) other such things on ATProto that aren’t necessarily Bluesky-related – see e.g. &lt;a href="https://whtwnd.com"&gt;WhiteWind&lt;/a&gt; or &lt;a href="https://leaflet.pub"&gt;Leaflet&lt;/a&gt; (blogging platforms), &lt;a href="https://tangled.org"&gt;Tangled&lt;/a&gt; (GitHub alternative), &lt;a href="https://frontpage.fyi"&gt;Frontpage&lt;/a&gt; (Hacker News style link aggregator), or &lt;a href="https://grain.social"&gt;Grain&lt;/a&gt; (photo sharing site). They use the same underlying mechanisms that are at the base of ATProto, but use separate data types, have different rules, goals, and UIs. How do we call these things as a whole, the different sets of “data types + rules + required servers + client apps” that define different use cases of the network?&lt;/p&gt;

&lt;p&gt;Bluesky team usually calls them “apps”, but I’m not a big fan of this term, because “app” kinda implies a client app, and that’s just one small piece of it. I&amp;nbsp;sometimes call them “services” – though it’s probably not perfect either, since it implies just the server part in turn. Suggestions welcome&amp;nbsp;:) (I’m mentioning this at the beginning, because this is something that many different parts are related to.)&lt;/p&gt;

&lt;p&gt;Personally, when I&amp;nbsp;say “the Bluesky app”, I&amp;nbsp;will generally mean the actual client app (mobile / webapp), not the “service”, and when I&amp;nbsp;say “Bluesky-specific”, I&amp;nbsp;will mean the “service”, not the company; and “Bluesky-hosted” will mean run by Bluesky the company. Hopefully in most cases, it can be guessed from context.&lt;/p&gt;

&lt;p&gt;BTW, the commonly accepted term for the whole shared “multiverse” of all ATProto apps is “The&amp;nbsp;Atmosphere”, or “ATmosphere” (though I&amp;nbsp;much prefer the former personally, the weird capitalization bugs me somehow&amp;nbsp;;). It was &lt;a href="https://bsky.app/profile/did:plc:bnqkww7bjxaacajzvu5gswdf/post/3k26nw6kwnh2e"&gt;coined&lt;/a&gt; by someone from the community, but was accepted by the team and is now mentioned on the &lt;a href="https://atproto.com/guides/glossary#atmosphere"&gt;official atproto site&lt;/a&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Let’s start with defining the various building pieces of the protocol:&lt;/p&gt;

&lt;p id="records"&gt;&lt;/p&gt;

&lt;h2&gt;Records &amp;amp; blobs&lt;/h2&gt;

&lt;p&gt;The most basic piece of the ATProto world is a &lt;strong&gt;record&lt;/strong&gt;. Records are basically JSON objects representing the data about a specific entity like a post or profile, organized in a specific way. A post/reply, repost, like, follow, block, list, entry on a list, user profile info – each of these is one record. Most public actions you take on Bluesky, like following someone or liking a post, are performed by creating a record of an appropriate type (or editing/deleting one created before).&lt;/p&gt;

&lt;p&gt;For example, this is a &lt;a href="https://shiitake.us-east.host.bsky.network/xrpc/com.atproto.repo.getRecord?repo=did:plc:vc7f4oafdgxsihk4cry2xpze&amp;amp;collection=app.bsky.feed.post&amp;amp;rkey=3ltxjiss3is2j"&gt;post record&lt;/a&gt;. This is one of the &lt;a href="https://lionsmane.us-east.host.bsky.network/xrpc/com.atproto.repo.getRecord?repo=did:plc:sflxm2fxohaqpfgahgdlm7rl&amp;amp;collection=app.bsky.feed.like&amp;amp;rkey=3luapblv3vd2i"&gt;likes of that post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Records are stored on disk and transferred between servers in a binary format called &lt;a href="https://cbor.io"&gt;CBOR&lt;/a&gt;, although in most API&amp;nbsp;endpoints they’re returned in a JSON form (they are equivalent, just different encodings of the same data).&lt;/p&gt;

&lt;p&gt;The key thing about records, which has very real consequences for user-facing features, is that you can only create and modify &lt;em&gt;your own&lt;/em&gt; records, not those owned by others (and there are no “shared” records at the moment, each record is owned by a specific account). This means that e.g. when you follow someone, you create a follow record on your account, and that other person can’t delete your record, which is why there’s currently no “soft-blocking” feature, i.e. you can’t make someone stop following you (though you can block them). There are workarounds though, as I’ll explain later in the &lt;a href="#appview"&gt;AppView section&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This also means that there’s often an unexpected assymetry between seemingly similar actions: for example, getting a list of people followed by person X is very simple (they’re all X’s records, so they’re all in one place), but getting a list of all followers of X is much harder (each record is in a different place!). This is something that the AppView helps with too, as we’ll see later.&lt;/p&gt;

&lt;p&gt;A second, complimentary way of storing user data is &lt;strong&gt;blobs&lt;/strong&gt;. Blobs are basically binary files, meant mostly for storing media like images and video. For example, here is a direct link to an &lt;a href="https://lab.martianbase.net/xrpc/com.atproto.sync.getBlob?did=did:plc:oio4hkxaop4ao4wz2pp3f4cr&amp;amp;cid=bafkreib7vmhsk7w36bmrlwi2mjgkkoq44xysdahi226re2a76rlmgamgvu"&gt;image blob&lt;/a&gt; showing a photo of when I&amp;nbsp;started writing this blog post. Blobs are stored on the same server as records, but somewhat separate from them, since it’s a different type of data.&lt;/p&gt;

&lt;p id="lexicons"&gt;&lt;/p&gt;

&lt;h2&gt;Lexicons&lt;/h2&gt;

&lt;p&gt;Each record belongs to a specific “record type” and stores its data organized in a specific structure, which defines what kinds of fields it can have with what types, what they mean, which are required, and so on – kind of like XML/JSON Schema. This schema definition which describes a given record type is called a &lt;strong&gt;lexicon&lt;/strong&gt; in ATProto. (If you’re curious why make a new standard, see threads e.g. &lt;a href="https://blue.mackuba.eu/skythread/?author=did:plc:ragtjsm2j2vknwkz3zp4oxrd&amp;amp;post=3juoxe37rez2q"&gt;here&lt;/a&gt;, &lt;a href="https://blue.mackuba.eu/skythread/?author=did:plc:ragtjsm2j2vknwkz3zp4oxrd&amp;amp;post=3kjgebkayik2g"&gt;here&lt;/a&gt;, or &lt;a href="https://blue.mackuba.eu/skythread/?author=did:plc:ragtjsm2j2vknwkz3zp4oxrd&amp;amp;post=3jvf7bakmm22h"&gt;here&lt;/a&gt;, or &lt;a href="https://www.pfrazee.com/blog/why-not-rdf"&gt;this blog post&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A lexicon needs to have an identifier (called &lt;strong&gt;NSID&lt;/strong&gt;, Namespace Identifier), which uses the reverse domain name format, e.g. &lt;code&gt;app.bsky.feed.post&lt;/code&gt;. All lexicons that are used to store the data of a specific app are usually grouped under the same prefix, e.g. Bluesky lexicons all start with &lt;code&gt;app.bsky&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The structure of a given lexicon’s records is defined in a special JSON file – for example, this file defines the &lt;a href="https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/post.json"&gt;app.bsky.feed.post lexicon&lt;/a&gt;. As you can see, this is the place which for example specifies that a post’s text can have at most 300 characters (more specifically, Unicode graphemes). This also means that you can’t create a different server which would make posts longer than 300 characters that would be Bluesky-compatible and displayed on &lt;a href="https://bsky.app"&gt;bsky.app&lt;/a&gt; – such posts would not pass the validation against the post record schema, and would be rejected by any server or client which performs such validation. Essentially, whover designs and controls the given lexicon, decides what kinds of data it can hold and any constraints on it. In order to store a different, incompatible type of data, you need to create a new lexicon (although you &lt;em&gt;can&lt;/em&gt; add additional fields to a record that aren’t defined in its lexicon; many third party apps are doing that, like e.g. &lt;a href="https://github.com/snarfed/bridgy-fed/issues/1092#issuecomment-2164027121"&gt;Bridgy Fed&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Lexicon name prefixes generally define boundaries between “apps” as in “services”, and between the “territory” that’s owned by different parties. The lexicons and endpoints defined by Bluesky are defined either under &lt;code&gt;app.bsky.*&lt;/code&gt; – these are things specific to Bluesky the microblogging service – or under &lt;code&gt;com.atproto.*&lt;/code&gt;, which are things meant to be used by all ATProto apps and services regardless of the use case. There are also a couple of other minor namespaces like &lt;code&gt;chat.bsky.*&lt;/code&gt; for the (centralized) DM service, and &lt;code&gt;tools.ozone.*&lt;/code&gt; for the open source &lt;a href="https://github.com/bluesky-social/ozone/"&gt;Ozone moderation tool&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The lexicon prefix is generally (in most cases) a good way to tell if a piece of the protocol is something Bluesky-specific (specific to the Bluesky service), or something general for all ATProto. There are no record types defined in &lt;code&gt;com.atproto&lt;/code&gt;, so things like post, profile, follow are all Bluesky-specific and under &lt;code&gt;app.bsky&lt;/code&gt;, as are APIs for e.g. searching users, getting timelines, custom feeds and so on. Meanwhile, &lt;code&gt;com.atproto&lt;/code&gt; APIs deal more with things like: info about a repository, fetching a repository, signing up for a new account, refreshing an access token, downloading a blob, etc.&lt;/p&gt;

&lt;p&gt;Third party developers and teams building apps on ATProto/Bluesky, which either extend Bluesky’s features or make something completely separate, use their own namespaces for new lexicons, like &lt;code&gt;blue.flashes&lt;/code&gt;, &lt;code&gt;social.pinksky&lt;/code&gt;, &lt;code&gt;events.smokesignal&lt;/code&gt;, &lt;code&gt;sh.tangled&lt;/code&gt;, and so on. (There is a lot of nuance to whether you should use your own lexicons or reuse or extend existing ones when building things, and there have been a lot of discussions about it on Bluesky, and even conference talks. A good starting point is &lt;a href="https://www.pfrazee.com/blog/lexicon-guidance"&gt;this blog post&lt;/a&gt; by Paul Frazee.)&lt;/p&gt;

&lt;p id="identity"&gt;&lt;/p&gt;

&lt;h2&gt;Identity&lt;/h2&gt;

&lt;p&gt;Each user is uniquely identified in the network with their &lt;strong&gt;Decentralized Identifier (DID)&lt;/strong&gt;. DIDs are a &lt;a href="https://www.w3.org/TR/did-1.0/"&gt;W3C standard&lt;/a&gt;, but (as I&amp;nbsp;understand) this standard mostly just defines a framework, and there can be many different “methods” of storing and resolving the identifiers, and each system that uses it can pick or create different types of those DIDs.&lt;/p&gt;

&lt;p&gt;The format of a DID is: &lt;code&gt;did:&amp;lt;type&amp;gt;:&amp;lt;…&amp;gt;&lt;/code&gt;, where the last part depends on the method. ATProto supports two types of DIDs, but in practice, almost everyone uses one of them, the “plc”. Each DID has a “&lt;strong&gt;DID document&lt;/strong&gt;”, a JSON file (&lt;a href="https://plc.directory/did:plc:oio4hkxaop4ao4wz2pp3f4cr"&gt;see mine&lt;/a&gt;) which describes the account – in ATProto at least, the document includes things such as: the assigned handles, the PDS server hosting the account, and some cryptographic keys.&lt;/p&gt;

&lt;p&gt;An important thing to note is that &lt;strong&gt;DIDs are permanent&lt;/strong&gt;; it’s the only thing that is permanent about your account, because something has to be. There needs to be some unique ID that all databases everywhere can use to identify you, which doesn’t change, and the DID is that ID. This means that you can’t change a DID of one type into another type later.&lt;/p&gt;

&lt;p&gt;The main DID method is &lt;code&gt;did:plc&lt;/code&gt;, where IIRC “plc” originally stood for “placeholder” (I&amp;nbsp;think it was meant to be temporary until something better is designed), and was later kind of retconned to mean “&lt;a href="https://github.com/did-method-plc/did-method-plc"&gt;Public Ledger of Credentials&lt;/a&gt;” 🙃 The DIDs of this type are identified by a random string of characters, which looks like this: &lt;code&gt;did:plc:vc7f4oafdgxsihk4cry2xpze&lt;/code&gt;. The DID documents of each DID are stored in a centralized service hosted at &lt;a href="https://plc.directory"&gt;plc.directory&lt;/a&gt; (Bluesky wants to eventually transfer the ownership to some external non-profit), which basically keeps a key-value store mapping a DID to a JSON file. It also keeps an “&lt;a href="https://plc.directory/did:plc:oio4hkxaop4ao4wz2pp3f4cr/log/audit"&gt;audit log&lt;/a&gt;” of the previous versions of the document (this means that, for example, the whole history of your old handles is available and you can’t erase it!). There’s also some cryptographic stuff there which, as I&amp;nbsp;understand it, lets anyone verify that everything in the database checks out (don’t ask me how).&lt;/p&gt;

&lt;p&gt;The other, rarely used method is &lt;code&gt;did:web&lt;/code&gt;. Those DIDs look like this: &lt;code&gt;did:web:witchcraft.systems&lt;/code&gt;, and the DID document is stored in a specific &lt;code&gt;.well-known&lt;/code&gt; path on the given hostname, in this case &lt;a href="https://witchcraft.systems/.well-known/did.json"&gt;witchcraft.systems&lt;/a&gt; (yes, that’s an actual TLD&amp;nbsp;;). It does not store an audit log/history like &lt;code&gt;plc&lt;/code&gt; does.&lt;/p&gt;

&lt;p&gt;The reason why it’s rarely used and not recommended, is because, first, it&amp;rsquo;s more complicated to create one (though that’s a solvable problem of course, see a &lt;a href="https://blog.smokesignal.events/posts/3lwopvsmtx22a-creating-a-did-method-web-identity-for-atprotocol"&gt;just published guide&lt;/a&gt;); but second and more importantly, since DIDs are permanent, this means that your account is permanently bound to that domain. You need to keep it accessible and not let it expire, or you lose the account – you can’t migrate it to &lt;code&gt;did:web:another.site&lt;/code&gt; at some point later. It gives you more independence, but at the cost of being tied to that domain you have, and this isn’t a tradeoff that most people are likely to want, and definitely not people who don’t understand what they’re getting into.&lt;/p&gt;

&lt;p&gt;If you’re fine with that choice, you can create a &lt;code&gt;did:web&lt;/code&gt; account and almost everything in Bluesky and ATProto should work exactly the same. &amp;ldquo;Almost&amp;rdquo;, because some services forget to implement that second code path, since it’s so rarely used 😉 but in that case, politely nudging the developer to fix the issue should help in most cases&amp;nbsp;:&gt;&lt;/p&gt;

&lt;p id="handles"&gt;&lt;/p&gt;

&lt;h4&gt;Handles&lt;/h4&gt;

&lt;p&gt;What DIDs enable is that since they act as the unique identifier, your handle doesn’t have to, like it does on Mastodon. I&amp;nbsp;can be &lt;code&gt;@mackuba.bsky.social&lt;/code&gt; one day, &lt;code&gt;@mackuba.eu&lt;/code&gt; the next day, and &lt;code&gt;@mackuba.martianbase.net&lt;/code&gt; the week after. All existing connections – follows &amp;amp; followers, my posts, likes, blocks, lists I’m on, mentions in posts, etc. all work as before, because they all reference the DID, not the handle. With mentions specifically it works kinda funny, because they use what’s called a “facets” system (see &lt;a href="#facets"&gt;later section&lt;/a&gt;), where the link target is specified separately from the displayed text. So you can have an old post saying “hey @mackuba.bsky.social”, where the handle in it links to my profile which is now named &amp;ldquo;@mackuba.eu&amp;rdquo;. The link still works, because it really links to the DID behind the scenes.&lt;/p&gt;

&lt;p&gt;Unlike on the Fediverse, the format of handles is just a hostname, not username + hostname. You assign a whole hostname to a specific account, and if you own any domain name, that can be your username (and if you own a well known domain name, it’s strongly recommended that you do, as a form of self-verification!).&lt;/p&gt;

&lt;p&gt;The handle to DID assignment is a two-way link – a DID needs to claim a given handle, and the owner of the domain needs to verify that they own that DID. On the DID side, this happens in the &lt;code&gt;alsoKnownAs&lt;/code&gt; field of the DID document (&lt;a href="https://plc.directory/did:plc:oio4hkxaop4ao4wz2pp3f4cr"&gt;see here in mine&lt;/a&gt;). On the domain side, there are two ways of verifying a handle, depending on what’s more convenient to you: either a DNS TXT entry, or a file on a &lt;code&gt;.well-known&lt;/code&gt; path.&lt;/p&gt;

&lt;p&gt;You might be wondering how handles like &lt;code&gt;*.bsky.social&lt;/code&gt; work – in this case, each such handle is its own domain name, and you can actually enter a domain like &lt;a href="https://aoc.bsky.social"&gt;aoc.bsky.social&lt;/a&gt; into a browser and it will redirect to a Bluesky profile on &lt;a href="https://bsky.app"&gt;bsky.app&lt;/a&gt;. Behind the scenes, this is normally handled by having a wildcard domain pointing to one service, which responds to HTTP requests on that &lt;code&gt;.well-known&lt;/code&gt; path by returning different DIDs, depending on the domain. That’s not only a &lt;code&gt;bsky.social&lt;/code&gt; thing – e.g. there’s now an open Blacksky PDS server which hands out &lt;code&gt;blacksky.social&lt;/code&gt; handles, and there are even “handle services” which &lt;em&gt;only&lt;/em&gt; give out handles – e.g. you can be &lt;a href="https://swifties.social"&gt;yourname.swifties.social&lt;/a&gt; if you want&amp;nbsp;;)&lt;/p&gt;

&lt;p&gt;One place where handle changes break things is (some) post URLs on &lt;a href="https://bsky.app"&gt;bsky.app&lt;/a&gt;. The official web client uses handles by default in permalinks, which means that if you link to a Bluesky post e.g. from a blog post and you change your handle later, that link will no longer work. You can however replace the handle after &lt;code&gt;/profile/&lt;/code&gt; with the user’s DID, and the router accepts such links just fine, they just aren’t used by default. So the form you’d want to use when putting links in a blog post or article (like the one you&amp;rsquo;re reading) would be something like: &lt;a href="https://bsky.app/profile/did:plc:ragtjsm2j2vknwkz3zp4oxrd/post/3llwrsdcdvc2s"&gt;https://bsky.app/profile/did:plc:ragtjsm2j2vknwkz3zp4oxrd/post/3llwrsdcdvc2s&lt;/a&gt;.&lt;/p&gt;

&lt;p id="at-uris"&gt;&lt;/p&gt;

&lt;h2&gt;AT URIs&lt;/h2&gt;

&lt;p&gt;Each record can be uniquely addressed with a specific &lt;strong&gt;URI&lt;/strong&gt; with the at:// scheme. The format of the URI&amp;nbsp;is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;at://&amp;lt;user_DID&amp;gt;/&amp;lt;lexicon_NSID&amp;gt;/&amp;lt;rkey&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Rkey&lt;/strong&gt; is an identifier of a specific record instance – a usually short alphanumeric string, e.g. Bluesky post rkeys look something like &lt;code&gt;3larljiybf22v&lt;/code&gt;. So a complete post URI&amp;nbsp;might look like this: &lt;code&gt;at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3larljiybf22v&lt;/code&gt;. You can look up at:// URIs in some record browser tools, e.g. &lt;a href="https://pdsls.dev/at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3larljiybf22v"&gt;PDSls&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;AT URIs are used for all references between records – quotes, replies, likes, mute list entries, and so on. If you look at &lt;a href="https://morel.us-east.host.bsky.network/xrpc/com.atproto.repo.getRecord?repo=did:plc:l3rouwludahu3ui3bt66mfvj&amp;amp;collection=app.bsky.feed.like&amp;amp;rkey=3lwctqgpttm2a"&gt;this like record&lt;/a&gt;, for example, its &lt;code&gt;subject.uri&lt;/code&gt; points to &lt;code&gt;at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post/3lv2b3f5nys2n&lt;/code&gt;, which is the URI&amp;nbsp;of a post record you can see &lt;a href="https://pdsls.dev/at://did:plc:vwzwgnygau7ed7b7wt5ux7y2/app.bsky.feed.post/3lv2b3f5nys2n"&gt;here&lt;/a&gt;. Since the URIs use DIDs in the first part, handle changes don’t affect such links.&lt;/p&gt;

&lt;p&gt;There is a great guide to AT URIs and DIDs and how to handle them written by Dan Abramov, you may want to check it out &lt;a href="https://overreacted.io/where-its-at/"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p id="repos"&gt;&lt;/p&gt;

&lt;h2&gt;User repositories&lt;/h2&gt;

&lt;p&gt;All user data (records and blobs) is stored in a &lt;strong&gt;repository&lt;/strong&gt; (or “repo”). The repository is identified by user’s DID, and stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;records, grouped by lexicon into so-called &lt;strong&gt;collections&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;blobs (stored separately from records)&lt;/li&gt;
&lt;li&gt;authentication data like access tokens, signing keys, hashed passwords etc.&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;Internally, an important part of how the repo stores user records is a data structure called “&lt;a href="https://en.wikipedia.org/wiki/Merkle_tree"&gt;Merkle Search Tree&lt;/a&gt;” – but this isn’t something that you need to understand when using the protocol, unless you’re working on a PDS/relay implementation (I&amp;nbsp;haven’t needed to get into it so far).&lt;/p&gt;

&lt;p&gt;You can download the records part of your (or anyone else’s!) repo as a bundle called a &lt;a href="https://bsky.app/profile/did:plc:fkasq7xtzrmlvz46c5trkrn3/post/3lkedsoq4vs2d"&gt;CAR file&lt;/a&gt;, a &lt;a href="https://ipld.io/specs/transport/car/"&gt;Content Addressed Archive&lt;/a&gt; (fun fact: the icon for the button in the Bluesky app which downloads a repo backup is the shape of a car 🚘).&lt;/p&gt;

&lt;p&gt;The cool part is that a repository stores all data of the given user, from *all* lexicons. Including third party developer lexicons. This means that if someone has their account hosted on Bluesky servers, but uses third party ATProto apps like Tangled or Grain, Bluesky lets them store these apps’ records like Grain photos or Tangled pull requests on the same server where it keeps their Bluesky posts. (And yes, of course someone made a &lt;a href="https://github.com/ziodotsh/atfile"&gt;lexicon/tool for storing arbitrary files&lt;/a&gt; on your Bluesky PDS… and did it in Bash, because why not 🙃)&lt;/p&gt;

&lt;p id="xrpc"&gt;&lt;/p&gt;

&lt;h2&gt;XRPC&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;XRPC&lt;/strong&gt; is the convention used for APIs in the ATProto network. The API&amp;nbsp;endpoints use the same naming convention as lexicon NSIDs, and they have URLs with paths in the format of &lt;code&gt;/xrpc/&amp;lt;nsid&amp;gt;&lt;/code&gt;, e.g. &lt;code&gt;/xrpc/app.bsky.feed.getPosts&lt;/code&gt;. There are similar &lt;a href="https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/getPostThread.json"&gt;lexicon definition files&lt;/a&gt; which specify what parameters are accepted/required by an endpoint and what types of data are returned in the JSON response. PDSes, AppViews, labellers and feed generators all implement the same kind of API, although with different subsets of specific endpoints. Third party apps don’t &lt;em&gt;have&lt;/em&gt; to use the same convention, but it’s generally a good idea, since it integrates better with the rest of the ecosystem.&lt;/p&gt;

&lt;p id="facets"&gt;&lt;/p&gt;

&lt;h2&gt;Rich text / facets&lt;/h2&gt;

&lt;p&gt;This one is kinda Bluesky-specific, but it’s pretty important to understand, and I&amp;nbsp;think you can reuse it for non-Bluesky apps too.&lt;/p&gt;

&lt;p&gt;The “&lt;strong&gt;facets&lt;/strong&gt;” system is something used for links and possibly rich text in future in Bluesky posts. It’s perhaps a little bit unintuitive at first, but it’s pretty neat and allows for a lot of flexibility.&lt;/p&gt;

&lt;p&gt;The way you handle links, mentions, or hashtags, is that they aren’t highlighted automatically, but you need to specifically mark some range of text as a link using the facets. A facet is a marking of some range of the post text (from-to) with a specific kind of link. If you look e.g. at &lt;a href="https://pdsls.dev/at://did:plc:257wekqxg4hyapkq6k47igmp/app.bsky.feed.post/3lnkwu24v5k2j"&gt;this post here&lt;/a&gt;, you can see that it has a facet marking the byte range 60-67 of the post text as a hashtag “ahoy25”. If there was no facet there, it would just render as normal unlinked text &amp;ldquo;#ahoy25&amp;rdquo; in the post (when you see that, it’s an easy tell that a post was made using some custom tool that’s in early stages of development). It works the same way for mention links and normal URL links.&lt;/p&gt;

&lt;p&gt;(If you’re curious why they implemented it this way, check out &lt;a href="https://www.pfrazee.com/blog/why-facets"&gt;this blog post&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Note that the displayed text in the marked fragment doesn’t have to match what the facet links to; this means that you can have links that just use some shorter text for the link instead of a part of the URL, in order to fit more text in one post (although in the official app, clicking such link triggers a warning popup first). E.g. some Hacker News bots commonly use this format, see &lt;a href="https://bsky.app/profile/did:plc:7dh44snmqoa4gyzv3652gm3j/post/3lmhylb375m2a"&gt;this post&lt;/a&gt;. The Bsky app doesn&amp;rsquo;t let you create such posts directly, but some other clients like &lt;a href="https://skeetdeck.pages.dev/decks/3lpzuhjwlxcns"&gt;Skeetdeck&lt;/a&gt; do.&lt;/p&gt;

&lt;p&gt;Facets are also used for URL shortening – if you just put a long URL in the text of a post made through the API, it will be neither shortened nor highlighted. You need to manually mark it with a facet, and manually shorten the displayed part to whatever length you want.&lt;/p&gt;

&lt;p&gt;Likely the most tricky part is that the from-to index numbers you need to use for the ranges are counted on a UTF-8 representation of the text string, but they’re counted in bytes and not in unicode scalars – this is somewhat of an unfortunate tech debt thing as I&amp;nbsp;understand, and it was made this way mostly because of JavaScript, which doesn’t operate on UTF-8 natively, only in the form of byte arrays. This means you need to be extra careful with the indexes.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Ok, now that we got through the basic pieces, let’s talk about servers:&lt;/p&gt;

&lt;p id="pds"&gt;&lt;/p&gt;

&lt;h2&gt;PDS&lt;/h2&gt;

&lt;p&gt;The original copy of all user data is stored on a server called &lt;strong&gt;PDS&lt;/strong&gt;, Personal Data Server. This is the “source of truth”. A PDS stores one or more user accounts and repos, handles user authentication, and often serves as an “entry point” to the network when connecting from client apps. Most network requests from the client are sent to your PDS, although only some of them are handled directly by the PDS, and the rest are proxied e.g. to the AppView.&lt;/p&gt;

&lt;p&gt;Each PDS has an XRPC API&amp;nbsp;with some number of endpoints for things like listing repositories, listing contents of each, looking up a specific record or blob, account authentication and management, and so on. It also has a websocket API&amp;nbsp;called a “&lt;strong&gt;firehose&lt;/strong&gt;” (the &lt;a href="https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/sync/subscribeRepos.json"&gt;subscribeRepos endpoint&lt;/a&gt;). The firehose streams all changes happening on a given PDS (from all repos) as a stream of “events”, where each event is an addition, edit, or deletion of a record in one of the repos, or some change related to an account, like handle change or deactivation.&lt;/p&gt;

&lt;p&gt;One of the most important features of ATProto is that &lt;strong&gt;an account is not permanently assigned to a PDS&lt;/strong&gt;. Unlike in ActivityPub, where if you sign up on &lt;code&gt;mastodon.social&lt;/code&gt;, your identifier is bound to that hostname forever and it can never change, because everything uses it as the unique ID, here the unique ID is the DID. The PDS host is assigned to a user in the DID document JSON (e.g. on &lt;a href="https://plc.directory"&gt;plc.directory&lt;/a&gt;), but you can migrate to a different PDS at any point, and at the moment there are even some fairly user-friendly tools available for doing that, like &lt;a href="https://atpairport.com"&gt;ATP Airport&lt;/a&gt; or &lt;a href="https://pdsmoover.com"&gt;PDS MOOver&lt;/a&gt; (although it’s still a bit unpolished at the moment, &lt;del&gt;and for now you can’t migrate back to Bluesky-hosted PDSes&lt;/del&gt;). In theory, you should even be able to migrate to a different PDS if your old PDS is dead or goes rogue, if you have prepared in advance (&lt;a href="https://www.da.vidbuchanan.co.uk/blog/adversarial-pds-migration.html"&gt;this is a bit more technical&lt;/a&gt;). If everything goes well, nobody even notices that anything has changed (you can’t even easily check in the app what PDS someone is on, although there are external tools for that, like &lt;a href="https://internect.info"&gt;internect.info&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Initially, during the limited beta in 2023, Bluesky only had one PDS, &lt;code&gt;bsky.social&lt;/code&gt;. In November 2023, several additional PDSes were created (also under Bluesky PBC control) and existing users were quietly all spread to a random one of those. At that point, the network was already “technically federated”, operating in the target architecture, although with access restricted to only Bluesky-run servers. This restriction was lifted in February 2024 with the &lt;a href="https://docs.bsky.app/blog/self-host-federation"&gt;public federation launch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since then, ATProto enthusiasts started setting setting up PDS servers for themselves, either creating alt/test accounts there, or moving their main accounts. As of August 2025, there around 2000 third party PDS servers, although most of them are very small – usually hosting one person’s main and/or test accounts, and maybe those of a couple of their friends. I&amp;nbsp;have &lt;a href="https://blue.mackuba.eu/directory/pdses"&gt;a list of them&lt;/a&gt; on my website, and there’s also a more complete list &lt;a href="https://github.com/mary-ext/atproto-scraping"&gt;here&lt;/a&gt; (mine excludes inactive PDSes and empty accounts).&lt;/p&gt;

&lt;p&gt;As you can see there, there&amp;rsquo;s one massive PDS for &lt;a href="https://fed.brid.gy"&gt;Bridgy Fed&lt;/a&gt;, the Bluesky-Mastodon bridge service, hosting around 30-40k bridged accounts from the Fediverse, Threads, Nostr, Flipboard, or the web (blogs); then some number of small to medium PDSes for various services, and a very long tail of servers with single-digit number of accounts. At this moment, large public PDS in the style of Fedi instances aren’t much of a thing yet, although there are at least a few communities working on setting up one (e.g. &lt;a href="https://www.blackskyweb.xyz"&gt;Blacksky&lt;/a&gt;, &lt;a href="https://northskysocial.com"&gt;Northsky&lt;/a&gt;, or &lt;a href="https://turtleisland.blog/2025/04/28/turtle-island-bluesky-pds/"&gt;Turtle Island&lt;/a&gt;). Blacksky specifically has &lt;a href="https://bsky.app/profile/did:plc:w4xbfzo7kqfes5zb7r6qv3rw/post/3lvtlqoef5c2z"&gt;opened up for migrations&lt;/a&gt; just last week and has now a few hundred real accounts.&lt;/p&gt;

&lt;p&gt;The vast majority of PDSes at the moment use the &lt;a href="https://github.com/bluesky-social/pds"&gt;reference implementation from Bluesky&lt;/a&gt; (written in TypeScript), but there are a few alternative implementations at various levels of maturity (Blacksky&amp;rsquo;s Rudy Fraser’s &lt;a href="https://github.com/blacksky-algorithms/rsky/tree/main/rsky-pds"&gt;rsky&lt;/a&gt; written in Rust, &lt;a href="https://github.com/haileyok/cocoon"&gt;cocoon&lt;/a&gt; in Go, or &lt;a href="https://github.com/DavidBuchanan314/millipds"&gt;millipds&lt;/a&gt; in Python). The official version is very easy to set up and very cheap to run – it’s bundled in Docker, and there’s basically one script you need to run and answer a few questions.&lt;/p&gt;

&lt;p&gt;As for the Bluesky-hosted PDSes, the number is currently in high double digits, and each of them hosts a few hundred thousands of accounts (!). And what’s more, they keep the record data in SQLite databases, one per account. And it works really well, go figure. The Bluesky PDSes are all given names of different kinds of mushrooms (like Amanita, Boletus or Shiitake), hence they are often called “mushroom servers”; you can see the full list e.g. &lt;a href="https://status.bsky.app"&gt;here&lt;/a&gt;. &lt;code&gt;bsky.social&lt;/code&gt; was left as a so-called “&lt;strong&gt;entryway server&lt;/strong&gt;”, which handles shared authentication for all Bluesky-hosted PDSes (it’s a private piece of Bluesky PBC infrastructure that’s not open source and not needed for independent PDS hosters).&lt;/p&gt;

&lt;p id="relay"&gt;&lt;/p&gt;

&lt;h2&gt;Relay&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;relay&lt;/strong&gt; is probably the piece of the ATProto architecture that’s most commonly misunderstood by people familiar with other networks like the Fediverse. It doesn’t help that both the Fediverse and Nostr also include servers called “relays”, but they serve a different purpose in each of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a relay in Nostr is a core piece of the architecture: your posts are uploaded to one or more relays that you have configured and are hosted there, where other users can fetch them from&lt;/li&gt;
&lt;li&gt;a relay in the Fediverse is an optional helper service that redistributes posts from some number of instances who have opted in to others, in order to make content more discoverable e.g. on hashtag feeds&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;In ATProto, a relay is a server which combines the firehose streams from all PDSes it knows about into one massive stream that includes every change happening anywhere on the network. Such full-network firehose is then used as the input for many other services, like AppViews, labellers, or feed generators. It serves as a convenient streaming API&amp;nbsp;to get e.g. all posts on the network to process them somehow, or all changes to accounts, or all content in general, from a single place.&lt;/p&gt;

&lt;p&gt;Initially, the relay was also expected to keep a complete archive of all the data on the network, from all repos, from the beginning of time. This requirement was later removed in the updates late last year, at least partially triggered by the drastic increase in traffic in November 2024, which overwhelmed Bluesky’s and third party servers for at least a few days. Currently, Bluesky’s and other relays are generally “non-archival”, meaning that they live stream current events (+ a buffer of e.g. last 24 or 36 hours), but don’t keep a full archive of all repos (this change has &lt;a href="https://whtwnd.com/bnewbold.net/3lo7a2a4qxg2l"&gt;massive lowered the resource requirements&lt;/a&gt; / cost of running a relay, making it much more accessible). An archival relay could always be set up too, but I’m not aware of any currently operating.&lt;/p&gt;

&lt;p&gt;Bluesky operates one main relay at &lt;a href="https://bsky.network"&gt;bsky.network&lt;/a&gt;, which is used as a data source for their AppView and pretty much everyone else in the ATProto ecosystem at the moment (internally, it’s really some kind of “load balancer” using the &lt;a href="https://github.com/bluesky-social/indigo/tree/main/cmd/rainbow"&gt;rainbow&lt;/a&gt; service, with a few real relay servers behind it).&lt;/p&gt;

&lt;p&gt;The relay code is &lt;a href="https://github.com/bluesky-social/indigo/"&gt;implemented in Go&lt;/a&gt;, and isn’t very hard to get up and running (especially the recent “1.1” update improved things quite a lot). Some people have been running alternative relay services privately for some time, and there is now e.g. a public relay run by Rudy Fraser at &lt;a href="https://atproto.africa"&gt;atproto.africa&lt;/a&gt; (with a custom implementation in Rust! 🦀), and a couple run by &lt;a href="https://bsky.app/profile/did:plc:hdhoaan3xa3jiuq4fg4mefid/post/3ltkh6bdo4ki5"&gt;Phil @bad-example.com&lt;/a&gt;. I’m also running &lt;a href="https://relay.feeds.blue"&gt;my own small relay&lt;/a&gt;, feeding content only from non-Bluesky PDSes.&lt;/p&gt;

&lt;p id="jetstream"&gt;&lt;/p&gt;

&lt;h4&gt;Jetstream&lt;/h4&gt;

&lt;p&gt;There is also a variant of a relay called &lt;a href="https://github.com/bluesky-social/jetstream"&gt;Jetstream&lt;/a&gt; – it&amp;rsquo;s a service that reads from a real CBOR relay and outputs a stream that&amp;rsquo;s JSON based, better organized, and much more lightweight (the full relay includes a lot of additional data that&amp;rsquo;s mostly used for cryptographic operations and other low-level stuff). For many simpler tools and services, it might make more sense to stream data from that one instead, if only to save bandwidth. (Bluesky runs a couple of instances listed there in the readme, but you can also run your own.)&lt;/p&gt;

&lt;p id="appview"&gt;&lt;/p&gt;

&lt;h2&gt;AppView&lt;/h2&gt;

&lt;p&gt;The terribly named &lt;strong&gt;AppView&lt;/strong&gt; is the second most important piece of the network after the PDS.&lt;/p&gt;

&lt;p&gt;The AppView is basically an API&amp;nbsp;server that serves processed data to client apps. It’s an equivalent of an API&amp;nbsp;backend (with the databases behind it) that you’d find on a classic social media site like Twitter. AppView streams all new data written on the network from the relay, and saves a copy of it locally in a processed, aggregated and optimized form. For example, an AppView backed by an SQL database could have a &lt;code&gt;posts&lt;/code&gt; table with a &lt;code&gt;text&lt;/code&gt; column, a &lt;code&gt;likes&lt;/code&gt; table storing all likes with a foreign key &lt;code&gt;post_id&lt;/code&gt;, probably also an integer &lt;code&gt;likes_count&lt;/code&gt; column in &lt;code&gt;posts&lt;/code&gt; for optimization, and so on.&lt;/p&gt;

&lt;p&gt;The AppView is designed to be able to easily give information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the latest posts from this user&lt;/li&gt;
&lt;li&gt;all the replies in a given thread organized in a tree&lt;/li&gt;
&lt;li&gt;most recent posts on the network with the hashtag #rubylang or mentioning “iOS 26”&lt;/li&gt;
&lt;li&gt;how many likes/reposts has a given post received, and who made them&lt;/li&gt;
&lt;li&gt;how many follows/followers does a given user have, and who are they&lt;/li&gt;
&lt;li&gt;is user A allowed to view or reply to a post from user B&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;All this data originates from users’ PDSes and has its original copy stored there, but the “raw” record don’t always allow you to access all information easily. For example, to find out how many likes a post has, you need to know all &lt;code&gt;app.bsky.feed.like&lt;/code&gt; records referencing it from other users, and each of those like records is stored in the liking user’s repo on that user’s PDS. Same with followers, as I&amp;nbsp;mentioned earlier in the section on records, or with building threads (again, different replies in one thread are hosted in different repos), or for basically any kind of search. So having this kind of API&amp;nbsp;with processed data from the entire network is essential for client apps and various tools and services built around Bluesky by other people.&lt;/p&gt;

&lt;p&gt;AppView also applies some additional rules to the data, sometimes overriding what people post into their PDSes, since anyone can technically post anything into their PDS. For example, the AppView prevents you from looking at the profiles of people who have blocked you, at least when you&amp;rsquo;re logged in. It also hides them from your followers list, even if they have a &lt;code&gt;follow&lt;/code&gt; record referencing you, making it seem like they don&amp;rsquo;t; and if they try to make an &lt;code&gt;app.bsky.feed.post&lt;/code&gt; replying to you (they &lt;em&gt;can&lt;/em&gt; create such record on their PDS!), it excludes such reply from feeds and threads, as if it never happened. Same goes for &amp;ldquo;thread gates&amp;rdquo; which lock access to threads, and so on.&lt;/p&gt;

&lt;p&gt;The AppView is one of the few components which &lt;em&gt;aren’t&lt;/em&gt; completely open source. Initially, the AppView used Postgres as its data store; &lt;em&gt;that&lt;/em&gt; version is still in the public repository. In late 2023, Bluesky has migrated to a “v2” version, which uses the NoSQL database &lt;a href="https://www.scylladb.com"&gt;ScyllaDB&lt;/a&gt; instead, to be able to handle the massive read traffic from many millions of concurrent users. The upper layer with the “business logic” is kept in the &lt;a href="https://github.com/bluesky-social/atproto/tree/main/packages/bsky"&gt;public repository&lt;/a&gt;, while the so called “dataplane” layer that interacts directly with Scylla is not. The reason is mostly that it’s built for a specific hardware setup they have and wouldn’t be directly usable by others, while it would add some unnecesary work for the team to publish it. It’s still possible to run the AppView with the &lt;a href="https://github.com/bluesky-social/atproto/tree/main/packages/bsky/src/data-plane"&gt;old Postgres-based data layer&lt;/a&gt; (and I&amp;nbsp;think the team uses that internally for development), it just can’t handle as much traffic as the current live version.&lt;/p&gt;

&lt;p&gt;This is the piece that’s hardest to run yourself, and one that requires the most resources. That said, a private AppView should be possible to run right now for &lt;a href="https://whtwnd.com/futur.blue/3ls7sbvpsqc2w"&gt;under $200/month&lt;/a&gt; – the biggest requirement is at least a few TB of disk space. The truly costly part is not collecting and storing all this data, but serving it to a huge number of users who would use it as a backend for the client app in daily use. An alternative full-network Bluesky AppView that is used by a few thousands of users shouldn’t be very hard to run, but to be able to serve millions, you’ll need a lot of hardware and something more custom than the Postgres-based version.&lt;/p&gt;

&lt;p&gt;There have also been some attempts at alternative implementations – the most advanced right now is &lt;a href="https://github.com/alnkesq/AppViewLite"&gt;AppViewLite&lt;/a&gt;, built in C#, which goes to great lengths to minimize the resource use.&lt;/p&gt;

&lt;p id="cdn"&gt;&lt;/p&gt;

&lt;h4&gt;CDN&lt;/h4&gt;

&lt;p&gt;A part of the AppView (at least the Bluesky one) is also a CDN for serving images &amp;amp; videos. The API&amp;nbsp;responses from e.g. &lt;code&gt;getTimeline&lt;/code&gt; or &lt;code&gt;getPostThread&lt;/code&gt; generally include links to any media on the Bluesky CDN hostname, not directly on the PDS, even though you &lt;em&gt;can&lt;/em&gt; fetch every blob from the PDS, since that&amp;rsquo;s the &amp;ldquo;source of truth&amp;rdquo; (although IIRC the Bluesky PDS implementation doesn&amp;rsquo;t set the CORS headers there). It&amp;rsquo;s recommended to access any media this way in order to not use too much bandwidth from the PDS.&lt;/p&gt;

&lt;p id="labellers"&gt;&lt;/p&gt;

&lt;h2&gt;Labellers&lt;/h2&gt;

&lt;p&gt;(Or “labelers” officially, but I&amp;nbsp;like the British spelling more here, sue me ¯\_(ツ)_/¯)&lt;/p&gt;

&lt;p&gt;We’re now getting to more Bluesky specific things (i.e. specific for the Bluesky-service, although some parts of it are ATProto-general and mentioned on the &lt;a href="https://atproto.com/specs/label"&gt;atproto.com site&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;labeller&lt;/strong&gt; is a moderation service for Bluesky (or other ATProto app), which can be run by third parties. Labellers emit labels, which are assigned to an account or a record (like a post). Each labeller defines its own set of labels, depending on what it’s focusing on; then, users can “subscribe” to a labeller and choose how they want to handle the labels it assigns: you can hide the labelled posts/users, mark them with a warning badge, or ignore given label.&lt;/p&gt;

&lt;p&gt;Labellers were initially designed to just do community moderation of unwanted content, e.g. you can have a service focused on fighting racism, transphobia, or right-wing extremism, and that service helps protect its users from some kinds of bad actors; or you can have one marking e.g. posts with political content, users who follow 20k accounts, or who post way too many hashtags. In practice, many &lt;a href="https://blue.mackuba.eu/labellers/"&gt;existing labellers&lt;/a&gt; are meant for self-labelling instead, letting you assign e.g. a country flag or some fun things like a D&amp;amp;D character class to yourself.&lt;/p&gt;

&lt;p&gt;The way it works technically is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a labeller either runs a firehose client pulling posts from the relay, or relies on reports from users and/or its operating team (usually using the &lt;a href="https://github.com/bluesky-social/ozone/"&gt;Ozone tool&lt;/a&gt; for that)&lt;/li&gt;
&lt;li&gt;labels, which are lightweight objects (&lt;em&gt;not&lt;/em&gt; ATProto records) are emitted from labeller’s special firehose stream (the &lt;a href="https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/label/subscribeLabels.json"&gt;subscribeLabels endpoint&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;the AppView listens to the label firehoses of all labellers it knows about, in addition to the relay stream, and records all received labels in its database&lt;/li&gt;
&lt;li&gt;when a logged in user pulls data like threads or timelines from the AppView, it adds relevant label info to the responses depending on which labellers the user follows&lt;/li&gt;
&lt;li&gt;the specific list of labellers whose labels should be applied is passed explicitly in API&amp;nbsp;requests in the &lt;code&gt;atproto-accept-labelers&lt;/code&gt; header (there is a “soft” limit of 20 labellers you can pass at a time, which is why the official app won’t let you subscribe to more)&lt;/li&gt;
&lt;li&gt;in the official app, Bluesky’s official moderation service (which is “just” another labeller) is hardcoded as one of those 20 and you can’t turn it off; when connecting from your own app or tool, you’re free to ignore it if you want&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;(Read more about labellers &lt;a href="https://mackuba.eu/2024/02/21/bluesky-guide/#labellers"&gt;here&lt;/a&gt;.)&lt;/p&gt;

&lt;p id="feeds"&gt;&lt;/p&gt;

&lt;h2&gt;Feed generators&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://mackuba.eu/2024/02/21/bluesky-guide/#feeds"&gt;Custom feeds&lt;/a&gt; are one of the coolest features of Bluesky. They let you create any kind of feed using any algorithm and let everyone on the platform use it (even as the default feed, if they want to).&lt;/p&gt;

&lt;p&gt;The way this system works is that you need to run a “&lt;strong&gt;feed generator&lt;/strong&gt;” service on your server. In that service, you expose an API&amp;nbsp;that the AppView can call, which returns a list of post at:// URIs selected by you however you want in response to a given request.&lt;/p&gt;

&lt;p&gt;A minimal feed service can be pretty simple – the API&amp;nbsp;is just three endpoints, two of which are static, and the third returns the post URIs. One &amp;ldquo;small&amp;rdquo; problem is that in order to return the post URIs, you need to have some info about posts stored up front, which in practice means that you almost always need to connect to a relay’s firehose stream and store some post data (of selected or all posts, depending on your use case).&lt;/p&gt;

&lt;p&gt;The flow is like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a feed record is uploaded to your repo, including metadata and location of the feed generator service, which lets other users find your feed&lt;/li&gt;
&lt;li&gt;when the user opens that feed in the app, the AppView makes a request to your service on their behalf&lt;/li&gt;
&lt;li&gt;your service looks at the request params and headers, and returns a list of posts it selected in the form of at:// URIs&lt;/li&gt;
&lt;li&gt;the AppView takes those URIs and maps them to full posts (so-called “hydration”), which it returns to the user&amp;rsquo;s app&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;How exactly those posts are selected to be returned in the given request is completely up to you, the only requirement is that these are posts that the AppView will have in its database, since you only send URIs, not actual post data. In most cases, feeds use some kind of keyword/regexp matching and chronological ordering, but you can even build very complex, AI-driven algorithmic “For You” style personalized feeds.&lt;/p&gt;

&lt;p&gt;You don&amp;rsquo;t necessarily have to code a feed service yourself and host it in order to have a custom feed – there are a few feed hosting services that don&amp;rsquo;t require technical knowledge to use, like &lt;a href="https://skyfeed.app"&gt;SkyFeed&lt;/a&gt; or &lt;a href="https://www.graze.social"&gt;Graze&lt;/a&gt;.&lt;/p&gt;

&lt;p id="clients"&gt;&lt;/p&gt;

&lt;h2&gt;Client apps&lt;/h2&gt;

&lt;p&gt;Ok, that’s technically not a server, but stay with me…&lt;/p&gt;

&lt;p&gt;The final piece that you need to fully enjoy Bluesky is the client app – a mobile/desktop one or a web frontend. Unlike on Fedi, where an instance software like Mastodon usually includes a built-in web frontend that is your main interface for accessing the service, the PDS doesn’t include anything like that, just a database and an API&amp;nbsp;(which also means it’s much more lightweight and needs less resources). All browsing is done through a separate client, and the client always does everything through the public API&amp;nbsp;– kind of like when you run a custom web client for Mastodon like &lt;a href="https://elk.zone"&gt;Elk&lt;/a&gt; or &lt;a href="https://phanpy.social"&gt;Phanpy&lt;/a&gt;, you connect it to your instance, and you view your timeline on &lt;a href="https://elk.zone"&gt;elk.zone&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So when you go to &lt;a href="https://bsky.app"&gt;bsky.app&lt;/a&gt;, that’s what you’re seeing – a web client that connects to your PDS (Bluesky-hosted or self-hosted) through the public API, no more, no less. The official app is built for both mobile platforms and for the web from a single React Native codebase (apparently React Native on the web and normal web React is not the same thing 🧐). This has allowed the still very small frontend team (and IIRC at first it was literally just Paul) to build the app for three platforms in any reasonable amount of time and maintain it going forward. The downside is that it’s kinda neither a great webapp nor a great mobile app… But the team is doing what they can to improve it, and it’s already much better than it used to be, and tbh more than good enough for me.&lt;/p&gt;

&lt;p&gt;There aren’t nearly as many alternative clients as there are for Mastodon, and none of them are &lt;em&gt;really&lt;/em&gt; great, but there are a few options; see the &lt;a href="https://mackuba.eu/2024/02/21/bluesky-guide/#apps"&gt;apps part of my Bluesky Guide&lt;/a&gt; blog post for links.&lt;/p&gt;

&lt;p id="dms"&gt;&lt;/p&gt;

&lt;h2&gt;DMs&lt;/h2&gt;

&lt;p&gt;Notice that I&amp;nbsp;haven’t mentioned DMs anywhere – that’s because they aren’t a part of the protocol at the moment. The Bluesky team wants to eventually add some properly implemented, end-to-end encrypted, secure DMs using some open standard, but they won’t be able to finish that in the short term, and a lot of people were asking for at least some simple version of DMs in the app. So they’ve decided as an interim solution to implement them as a fully centralized, closed source service. It is accessible to third-party Bluesky clients through the API&amp;nbsp;(the &lt;code&gt;chat.bsky.*&lt;/code&gt; namespace), but it’s not something you can run yourself. The team is &lt;a href="https://bsky.app/profile/did:plc:44ybard66vv44zksje25o7dz/post/3lacrutxhio2h"&gt;very open&lt;/a&gt; about the fact that it’s not a proper replacement for something like Signal, and that for sensitive communication, you should ideally just use it for swapping contacts on Signal on iMessage and move the conversation there. They also kinda don’t want to spend too much time adding features there, because it’s considered a temporary solution, so it’s pretty basic in terms of available features.&lt;/p&gt;

&lt;p&gt;There are also a few other closed-source helper services, like the “cardyb” they use for generating link card details, or the video service for preprocessing videos, but they’re all specific to some Bluesky use cases only and not strictly necessary to use.&lt;/p&gt;

&lt;hr /&gt;

&lt;p id="flow"&gt;&lt;/p&gt;

&lt;h2&gt;How it all fits together&lt;/h2&gt;

&lt;p&gt;So the flow and hierarchy is like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;client app&lt;/strong&gt; you use creates new records as a result of actions you take (new posts, likes, follows), and saves them into your PDS&lt;/li&gt;
&lt;li&gt;your &lt;strong&gt;PDS&lt;/strong&gt; emits events on its firehose with the record details&lt;/li&gt;
&lt;li&gt;Bluesky &lt;strong&gt;relay&lt;/strong&gt; and other relays are connected to the firehoses of each PDS they know about (your PDS generally needs to ask them to connect using the &lt;code&gt;PDS_CRAWLERS&lt;/code&gt; ENV variable), and they pass those events to their output firehose&lt;/li&gt;
&lt;li&gt;the Bluesky &lt;strong&gt;AppView&lt;/strong&gt; (and other AppViews) listen to the firehose of their selected relay (though it could be multiple relays, or it could even just stream directly from PDSes, but in practice this will normally be one trusted relay)&lt;/li&gt;
&lt;li&gt;the AppView gets events including your records, and if they are relevant, saves the data to its internal database in some appropriate representations&lt;/li&gt;
&lt;li&gt;when other users browse Bluesky in their client apps, they load timelines, feeds and threads from the AppView, which returns info about your post from that database it saved it to&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;Additionally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;feed generators&lt;/strong&gt; run by third party feed operators also stream data from Bluesky&amp;rsquo;s or some other relay and save it locally, so they can respond to feed requests from the AppView&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;labellers&lt;/strong&gt; also stream data from Bluesky&amp;rsquo;s or some other relay, and emit labels on their firehoses, which get sent to the AppView (note: there is no official &amp;ldquo;labeller relay&amp;rdquo; sitting between labellers and the AppView, although one third party dev &lt;a href="https://bsky.app/profile/did:plc:w4xbfzo7kqfes5zb7r6qv3rw/post/3lrgs3itqyc2q"&gt;wrote one&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;Note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDSes &lt;strong&gt;do not connect to each other directly&lt;/strong&gt;, and they don&amp;rsquo;t store posts of users from other PDSes, only their own&lt;/li&gt;
&lt;li&gt;although right now basically everyone uses the Bluesky relay and AppView, anyone &lt;em&gt;can&lt;/em&gt; set up their own alternative relays and AppViews, which feed from all or any subset of known PDSes&lt;/li&gt;
&lt;li&gt;PDS chooses which relays to ask to connect, but relays can also connect by themselves to a PDS or another relay; AppView chooses which relay(s) it streams data from; and PDS chooses which AppView it loads timelines &amp;amp; threads from&lt;/li&gt;
&lt;li&gt;it&amp;rsquo;s absolutely possible and expected that two users using different PDSes, which use separate AppViews feeding from separate relays will be able to talk to each other and see each other&amp;rsquo;s responses on their own AppView, as long as the users aren&amp;rsquo;t banned on the other user&amp;rsquo;s infrastructure&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;The metaphor that&amp;rsquo;s often used to describe these relationship is that PDSes are like websites which publish some blog posts, and relays &amp;amp; AppViews are like search engines which crawl and index the web, and then let you look up results in them. In most cases, a website should be indexed and visible in all/most available search engines.&lt;/p&gt;

&lt;hr /&gt;

&lt;p id="next"&gt;&lt;/p&gt;

&lt;h2&gt;Where to go next&lt;/h2&gt;

&lt;p&gt;And that’s about it – I&amp;nbsp;think with the above, you should have a pretty good grasp of the big picture of ATProto architecture and all the specific parts of it. Now, if you want to start playing with the protocol and building some things on it, a lot will depend on what specifically you want to build and using what languages/technologies:&lt;/p&gt;

&lt;p id="sdk"&gt;&lt;/p&gt;

&lt;h4&gt;SDKs:&lt;/h4&gt;

&lt;p&gt;Two languages are officially supported by Bluesky:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript/TypeScript, in which most of their code is written (see the &lt;a href="https://github.com/bluesky-social/atproto/tree/main/packages"&gt;packages folder&lt;/a&gt; in the &lt;code&gt;atproto&lt;/code&gt; repo)&lt;/li&gt;
&lt;li&gt;Go, which is used in some backend pieces like the relay, or the &lt;a href="https://github.com/bluesky-social/goat"&gt;goat&lt;/a&gt; command line tool used e.g. for PDS migrations (see the &lt;a href="https://github.com/bluesky-social/indigo"&gt;&lt;code&gt;indigo&lt;/code&gt; repo&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;For Python, there is a &lt;a href="https://atproto.blue"&gt;pretty full-featured SDK created by Marshal&lt;/a&gt;, which is the only third party SDK &lt;a href="https://atproto.com/sdks"&gt;officially endorsed&lt;/a&gt; by the Bluesky team.&lt;/p&gt;

&lt;p&gt;For other languages, I&amp;nbsp;have a website called &lt;a href="https://sdk.blue"&gt;sdk.blue&lt;/a&gt;, which lists all libraries and SDKs I&amp;nbsp;know about, grouped by language. As you can see, there is something there for most major languages; I&amp;rsquo;ve built and maintain a group of &lt;a href="https://sdk.blue/#ruby"&gt;Ruby gems&lt;/a&gt; myself. If you want to use a language that doesn’t have any libraries yet, it’s really not that hard to make one from scratch – for most things you just need an HTTP client and a JSON parser, and maybe a websocket client.&lt;/p&gt;

&lt;p id="docs"&gt;&lt;/p&gt;

&lt;h4&gt;Docs:&lt;/h4&gt;

&lt;p&gt;There is quite a lot of official documentation, although it’s a bit spread out and sometimes not easy to find.&lt;/p&gt;

&lt;p&gt;The places to look in are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://atproto.com"&gt;atproto.com&lt;/a&gt; – the official AT Protocol website; a bit more formal documentation about the elements of the protocol, kind of like what I&amp;nbsp;did here, but with much more info and detailed specifications of each thing&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/docs/get-started"&gt;docs.bsky.app&lt;/a&gt; – more practical documentation with guides and examples of specific use cases in TS &amp;amp; Python (roll down the sections in the sidebar); it shows examples of how to make a post, upload a video, how to connect to the firehose, how to make a custom feed, etc.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/blog"&gt;docs.bsky.app/blog&lt;/a&gt; – developer blog with updates about protocol changes&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.bsky.app/docs/category/http-reference"&gt;HTTP reference&lt;/a&gt; – a reference of all the API&amp;nbsp;endpoints&lt;/li&gt;
&lt;li&gt;something that I&amp;nbsp;also find useful is to have the &lt;a href="https://github.com/bluesky-social/atproto"&gt;atproto repo&lt;/a&gt; checked out locally and opened in the editor, and look things up in the JSON files from the &lt;a href="https://github.com/bluesky-social/atproto/tree/main/lexicons"&gt;/lexicons folder&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;And a few other articles that might work better for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“&lt;a href="https://atproto.com/articles/atproto-for-distsys-engineers"&gt;ATProto for distributed systems engineers&lt;/a&gt;”, Bluesky’s technical overview of the server and data flow architecture&lt;/li&gt;
&lt;li&gt;“&lt;a href="https://atproto.com/articles/atproto-ethos"&gt;ATProto Ethos&lt;/a&gt;”, also on the Bluesky blog, based on a conference talk&lt;/li&gt;
&lt;li&gt;&amp;ldquo;&lt;a href="https://marvins-guide.leaflet.pub/3lyqxqbbqkc2p"&gt;What the hell is the atmosphere anyway&lt;/a&gt;: A slightly less technical intro to the technical side of Bluesky&amp;rdquo;, by Bailey Townsend (Sep 2025)&lt;/li&gt;
&lt;li&gt;&amp;ldquo;&lt;a href="https://overreacted.io/open-social/"&gt;Open Social&lt;/a&gt;&amp;rdquo; – a high-level overview of how ATProto brings back the openness of the original pre Web 2.0 web, by Dan Abramov (Sep 2025)&lt;/li&gt;
&lt;li&gt;&amp;ldquo;&lt;a href="https://overreacted.io/where-its-at/"&gt;Where It&amp;rsquo;s at://&lt;/a&gt;&amp;rdquo; – a well written guide to AT URIs and DIDs, by Dan Abramov (Oct 2025)&lt;/li&gt;
&lt;li&gt;the &amp;ldquo;&lt;a href="https://atproto.com/guides/applications"&gt;Statusphere&lt;/a&gt;&amp;rdquo; app example on atproto.com&lt;/li&gt;
&lt;/ul&gt;


&lt;p id="community"&gt;&lt;/p&gt;

&lt;h4&gt;Community:&lt;/h4&gt;

&lt;p&gt;Someone said recently that “&lt;em&gt;bsky replies are the only real documentation for ATProto&lt;/em&gt;”, and honestly, they’re not wrong. We have a great community of third party developers now, building their own tools, apps, libraries, services, even organizing &lt;a href="https://ahoy.eu"&gt;conferences&lt;/a&gt;. If you’re starting out and you have any questions, just ask and someone will probably help, and some of the Bluesky team developers are also very active in Bluesky threads, answering questions and clarifying things. So a lot of such knowledge that&amp;rsquo;s not necessarily found in the official docs can be found somewhere on Bluesky.&lt;/p&gt;

&lt;p&gt;The two places I&amp;nbsp;recommend looking at are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &amp;ldquo;ATProto Touchers&amp;rdquo; Discord chat – ping me or some other developer for an invite&amp;nbsp;:)&lt;/li&gt;
&lt;li&gt;my &lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/atproto"&gt;ATProto feed&lt;/a&gt; on Bluesky, which tries to catch any ATProto development discussions – it should include posts with any mention of “ATProto” or things like “AppView” or various API&amp;nbsp;names and technical terms, or you can use &lt;code&gt;#atproto&lt;/code&gt; or &lt;code&gt;#atdev&lt;/code&gt; hashtag to be sure&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;Also, there’s a fantastic newsletter called &lt;a href="https://connectedplaces.online"&gt;Connected Places&lt;/a&gt; (formerly Fediverse Report) by Laurens Hof, who publishes two separate editions every week, about what’s happening in Bluesky/ATProto and in the Fediverse (and *a lot* of things are happening).&lt;/p&gt;

&lt;p id="ideas"&gt;&lt;/p&gt;

&lt;h4&gt;Ideas:&lt;/h4&gt;

&lt;p&gt;Some easy ways to start tinkering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use one of the &lt;a href="https://sdk.blue"&gt;existing libraries for your favorite language&lt;/a&gt; and make a website or command-line tool which loads some data from the AppView or PDS: load and print timelines, calculate statistics, browse contents of PDSes and repos, etc.&lt;/li&gt;
&lt;li&gt;make a bot that posts something (not spammy!)&lt;/li&gt;
&lt;li&gt;make a simple custom feed service using &lt;a href="https://docs.bsky.app/docs/starter-templates/custom-feeds"&gt;one of the available templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;connect to the relay firehose and print or record some specific types of data&lt;/li&gt;
&lt;/ul&gt;


&lt;p id="tools"&gt;&lt;/p&gt;

&lt;h4&gt;Tools:&lt;/h4&gt;

&lt;p&gt;And a couple of tools which will certainly be useful in development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://internect.info"&gt;internect.info&lt;/a&gt; – look up an account by handle/DID and see details like assigned PDS or handle history&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pdsls.dev"&gt;PDSls&lt;/a&gt; – PDS and repository browser, lets you look up repos by account DID or records by at:// URI&amp;nbsp;(there are a few others, but this one is most popular)&lt;/li&gt;
&lt;/ul&gt;


&lt;hr /&gt;

&lt;p id="changelog"&gt;&lt;/p&gt;

&lt;h3&gt;Changelog:&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;15 Oct 2025&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;added link to Dan&amp;rsquo;s AT URI&amp;nbsp;guide&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;&lt;strong&gt;26 Sep 2025&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can now migrate back to Bluesky&amp;rsquo;s PDSes&lt;/li&gt;
&lt;li&gt;added links to Bailey Townsend&amp;rsquo;s and Dan Abramov&amp;rsquo;s blog posts&lt;/li&gt;
&lt;li&gt;some small corrections&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  <entry>
    <id>https://mackuba.eu/2025/06/30/socials-2025/</id>
    <title>Social media update 2025</title>
    <published>2025-06-30T14:43:43Z</published>
    <updated>2025-06-30T14:43:43Z</updated>
    <link href="https://mackuba.eu/2025/06/30/socials-2025/"/>
    <content type="html">

&lt;div class="hide-in-intro"&gt;
  &lt;p class="image noborder"&gt;&lt;img src="https://mackuba.eu/images/posts/bernie.jpg?1771213129" width="360" alt="Bernie asking: I&amp;nbsp;am once again asking if you could just turn on Bridgy"&gt;&lt;/p&gt;
&lt;/div&gt;


&lt;p&gt;So here we are, halfway through 2025, a bit over 2.5 years after the Eloncalypse… For better or worse, the Twitter as we knew it in the 2010s and the communities we had there are mostly gone. But it doesn’t feel like we’ve all settled on anything comparable.&lt;/p&gt;

&lt;p&gt;If you’re a software developer who was active on Twitter before, by now you’ve almost certainly tried at least one of the alternatives – Mastodon, Bluesky, and Threads, and you’re probably posting actively on at least one of these, but probably not on all of them. The problem is that nobody has enough mental space to be active on 3-4 similar social networks, so we’ve split into different camps which only partially overlap. You’re probably still missing some friends from Twitter and some interesting content. It’s all a bit in flux and a bit of a mess.&lt;/p&gt;

&lt;p&gt;Myself, I’ve basically left Twitter; I&amp;nbsp;haven’t spent much time on Threads (among other reasons, it was unavailable in Europe for a long time); and I’m mostly hanging out on Bluesky and somewhat on Mastodon.&lt;/p&gt;

&lt;p&gt;So where do we go from here?&lt;/p&gt;
&lt;p&gt;Obviously everyone has their own take on that, this is just mine. But I&amp;nbsp;really think we should all try to make an effort to focus on the widely understood “open social”, or what Laurens Hof from Fediverse Report now calls “&lt;a href="https://connectedplaces.online/connected-places-intro/"&gt;Connected places&lt;/a&gt;”. That means &lt;strong&gt;Bluesky and Mastodon/Fediverse&lt;/strong&gt; (with emphasis on &lt;em&gt;&amp;ldquo;and&amp;rdquo;&lt;/em&gt;), and to some degree maybe also Threads, although that depends on how their integration with ActivityPub progresses (and it’s looking more and more like they &lt;a href="https://bsky.app/profile/did:plc:esmiuxk53vmsllayghrq676w/post/3lseuteuuf22j"&gt;aren’t very serious about it&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;My advice:&lt;/p&gt;

&lt;p&gt;→ If you’re currently cross-posting or bridging between &lt;strong&gt;Mastodon and Bluesky&lt;/strong&gt;: awesome! ❤️&lt;/p&gt;

&lt;p&gt;→ If you’re active on &lt;strong&gt;Mastodon&lt;/strong&gt;, but currently ignoring or forgot about Bluesky: &lt;strong&gt;please&lt;/strong&gt; reconsider it. I&amp;nbsp;know that these two communities have a lot of differences between them, and we love to hate each other (I&amp;nbsp;fully admit I&amp;rsquo;m guilty of that myself). It’s likely you prefer one or the other of these for various reasons, and you might not be a fan of the other one. But I&amp;nbsp;think it’s clear at this point that none of them will disappear in the near-term at least or replace the other for everyone.&lt;/p&gt;

&lt;p&gt;It would be great if we all made some effort to connect to the other side, for those who like it more there. What are the options? Depending on what’s more convenient to you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;there are some native apps which let you post to two or more services in parallel, e.g. &lt;a href="https://croissantapp.com"&gt;Croissant&lt;/a&gt;, &lt;a href="https://openvibe.social"&gt;Openvibe&lt;/a&gt; or &lt;a href="https://mszpro.com/sorasns"&gt;SoraSNS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;most social media management services like &lt;a href="https://buffer.com"&gt;Buffer&lt;/a&gt; now support both Fediverse and Bluesky, so you can use that to post to both, including scheduling etc. There are several others like this, and they usually have some free plans.

&lt;ul&gt;
&lt;li&gt;e.g. &lt;a href="https://fedica.com"&gt;Fedica&lt;/a&gt; was one of the ones that had support for Bluesky from very early on&lt;/li&gt;
&lt;li&gt;also my friend from my first job, &lt;a href="https://solnic.dev"&gt;Peter Solnica&lt;/a&gt; (known from some Ruby libraries like DataMapper/ROM, dry-rb, Hanami, and now some Elixir libs too) is building his own called &lt;a href="https://justcrosspost.app"&gt;JustCrossPost&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I&amp;nbsp;use a little tool in Ruby I&amp;nbsp;wrote for myself named &lt;a href="https://github.com/mackuba/tootify"&gt;tootify&lt;/a&gt;, which lets me selectively cross-post relevant posts to Mastodon, almost effortlessly: I&amp;nbsp;post on Bluesky and then “like” my own post if I&amp;nbsp;want it to be copied to Mastodon (which clears the like). So this way I&amp;nbsp;can cross-post e.g. cat photos or iOS related posts, but skip Bluesky-specific content.&lt;br/&gt;
There are probably similar tools going in the other direction, although it’s a bit more complicated this way because of the post length limits (usually 500 on Mastodon vs. 300 on Bluesky).&lt;/li&gt;
&lt;li&gt;I&amp;nbsp;know some people have also written some iOS Shortcuts, scripts, browser extensions etc. (I&amp;nbsp;don&amp;rsquo;t have any links at hand)&lt;/li&gt;
&lt;li&gt;and last but not least, there’s &lt;a href="https://fed.brid.gy"&gt;Bridgy Fed&lt;/a&gt;: basically enable it once by following the bridge account, and you can forget about it. It creates a &amp;ldquo;mirror&amp;rdquo; account of yours on the other side, but when people interact with it there, the likes/reposts/comments go back to you (as long as that other person also has the bridge enabled).&lt;/li&gt;
&lt;li&gt;there are probably other options too, let me know in the comments&amp;nbsp;:)&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;→ If you’re active on &lt;strong&gt;Bluesky&lt;/strong&gt;: wonderful! ☺️ But again, all of the above applies. Don’t forget about the friends and strangers who prefer the elephant site 🦣. At the very least, enable the Bridgy bridge.&lt;/p&gt;

&lt;p&gt;→ If you do have both Mastodon and Bluesky accounts, but you&amp;rsquo;ve decided that you want to use them for &lt;strong&gt;different kind of content&lt;/strong&gt; (e.g. tech vs. non-tech)… please reconsider, in light of what I&amp;nbsp;wrote above. Most of the people who know you online would probably prefer to follow you in one or the other place they like more, not to have to follow you &lt;em&gt;everywhere&lt;/em&gt; in parallel. (Though nobody says you can&amp;rsquo;t have e.g. two different accounts which are both bridged.)&lt;/p&gt;

&lt;p&gt;→ If you had a bridged account, but turned off the bridge because you prefer to be posting there directly, but you don’t really post there directly (you know who you are 😛) – I&amp;nbsp;am begging you, please either figure out some cross-posting solution (see above), or enable the bridge 🙏🏻&lt;/p&gt;

&lt;p&gt;→ If you’re mostly active on &lt;strong&gt;Threads&lt;/strong&gt; (is that a thing?…) – turn on the &lt;a href="https://help.instagram.com/760878905943039"&gt;fediverse sharing option&lt;/a&gt; (it might not be available in the EU though, according to the support article?), and follow the Bridgy account to enable bridging to Bluesky (there aren’t many accounts connected like this, but it &lt;a href="https://bsky.app/profile/shnarfed.threads.net.ap.brid.gy"&gt;&lt;em&gt;should&lt;/em&gt; work&lt;/a&gt;, let me know if you have any problems).&lt;/p&gt;

&lt;p&gt;→ If you’re still mostly active on &lt;strong&gt;Twitter&lt;/strong&gt; (I&amp;nbsp;see you 👀 and I&amp;nbsp;am kinda judging you)… please rethink it. I&amp;nbsp;mean… you really don’t mind helping Elon and his buddies? I&amp;nbsp;know, there’s more content there, more engagement (&lt;a href="https://bsky.social/about/blog/11-29-2024-engagement"&gt;maybe&lt;/a&gt;), big accounts are still there, news is still there, more people talking there about startups, AI, indie dev or whatever. But wouldn’t you want to change that? Wouldn’t you prefer to use and build on a network with an open API&amp;nbsp;that isn’t controlled by one rich American far-right guy&lt;a href="#footnote1"&gt;1)&lt;/a&gt; who thinks he’s the president of the world?… Where you don’t see a full screen ad every few posts, and don&amp;rsquo;t need to pay a fuckton of dollars to build some fun tool on it? We need to make an effort to move away from there. Someone’s gotta start, and then maybe others will come.&lt;/p&gt;

&lt;p&gt;→ If you&amp;rsquo;re posting &lt;em&gt;both&lt;/em&gt; on Twitter and on Mastodon/Bluesky, then, well… it&amp;rsquo;s ok I&amp;nbsp;guess 🙃 I&amp;nbsp;think the most important thing is to let the people who do want to move away from Twitter completely, have the content they want to follow in the new place. That&amp;rsquo;s the first step. A natural second step is then to start decreasing the amount of content that there is on Twitter, so people have more incentive to look for it elsewhere, but I&amp;nbsp;understand if not everyone is ready for that step yet.&lt;/p&gt;

&lt;p&gt;→ If you tried Mastodon and/or Bluesky before, but you felt like it was &lt;strong&gt;too empty there&lt;/strong&gt;, you didn&amp;rsquo;t have enough content to read or you felt like you were shouting into the void – please give it another try, and please give it some time. Twitter also wasn&amp;rsquo;t immediately the place you remember from the first day you signed up, was it? You need to put in some effort, like you did everywhere else. Some tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on Bluesky: find some good &lt;a href="/2024/02/21/bluesky-guide/#feeds"&gt;custom feeds&lt;/a&gt; and/or &lt;a href="https://blueskydirectory.com/starter-packs"&gt;starter packs&lt;/a&gt; to follow&lt;/li&gt;
&lt;li&gt;on Mastodon: follow some hashtags, and use hashtags when posting to get more reach (but within reason!). On a private instance, this gets a bit more tricky due to the ActivityPub architecture, so it might make sense to start on a larger one at first.&lt;/li&gt;
&lt;li&gt;on both: try to find some people you recognize from Twitter, and then look who they follow or repost, and recursively look through their profiles too. Also, interact with people, give likes/faves, good replies, and so&amp;nbsp;on.&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;→ If you&amp;rsquo;re on &lt;strong&gt;Nostr&lt;/strong&gt;, there are some kind of Nostr ↔ Fedi bridges or gateways, and you can use those + Bridgy to reach Bluesky too (e.g. here&amp;rsquo;s a &lt;a href="https://bsky.app/profile/npub1wmr34t36fy03m8hvgl96zl3znndyzyaqhwmwdtshwmtkg03fetaqhjg240.momostr.pink.ap.brid.gy"&gt;Nostr account&lt;/a&gt; &amp;ldquo;double-bridged&amp;rdquo; to Bluesky).&lt;/p&gt;

&lt;p&gt;→ If you’ve moved to something like &lt;a href="https://micro.blog"&gt;Micro.blog&lt;/a&gt;, or just blogging – that’s also cool! But I&amp;nbsp;hope you’re somehow posting the links to Bluesky &amp;amp; Fedi too&amp;nbsp;:)&lt;/p&gt;

&lt;p&gt;→ If you’ve just given up on microblogging or social media in general… I&amp;nbsp;understand. It’s probably not a bad choice in the current times. I&amp;nbsp;hope we somehow meet again someday, digitally or in person 🩵&lt;/p&gt;

&lt;p&gt;So let’s connect, let’s build bridges. But also, let’s finally help the X-shaped zombie die 🧟‍♂️&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;(You can find me on Bluesky at &lt;a href="https://bsky.app/profile/mackuba.eu"&gt;@mackuba.eu&lt;/a&gt; (I&amp;nbsp;have my private PDS at &lt;a href="https://lab.martianbase.net"&gt;lab.martianbase.net&lt;/a&gt;), which is my main account these days, and on Mastodon at &lt;a href="https://martianbase.net/@mackuba"&gt;mackuba@martianbase.net&lt;/a&gt; (also a private instance), where I’m cross-posting a good chunk of the posts from Bluesky using Tootify. If you want to see everything including some shitposting, politics and ATProto-specific posts, the Bluesky account is also bridged as &lt;code&gt;mackuba.eu@bsky.brid.gy&lt;/code&gt;. Twitter account &lt;a href="https://twitter.com/kuba_suder"&gt;@kuba_suder&lt;/a&gt; is left as an archive, because I&amp;nbsp;don’t like deleting useful content from the Internet, but I&amp;nbsp;don’t use it anymore. Lately I&amp;rsquo;ve been also blogging somewhat more regularly than here on my new &lt;a href="https://journal.mackuba.eu"&gt;&amp;ldquo;journal&amp;rdquo; Micro.blog&lt;/a&gt;.)&lt;/p&gt;

&lt;p class="footnote" id="footnote1"&gt;1) If you now want to write a comment mentioning someone with the initials &amp;ldquo;J.D.&amp;rdquo;, don&amp;rsquo;t even think about it! 🫠&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://mackuba.eu/2025/02/04/micro-blog-journal/</id>
    <title>Micro.blog journal</title>
    <published>2025-02-04T16:00:05Z</published>
    <updated>2025-02-04T16:00:05Z</updated>
    <link href="https://mackuba.eu/2025/02/04/micro-blog-journal/"/>
    <content type="html">&lt;p&gt;&lt;strong&gt;Update 17.11.2025&lt;/strong&gt;: I&amp;rsquo;ve migrated this journal blog now from Micro.blog to &lt;a href="https://leaflet.pub"&gt;Leaflet&lt;/a&gt;, a new blogging service built on top of Bluesky&amp;rsquo;s ATProto. I&amp;nbsp;wrote about this &lt;a href="https://lab.mackuba.eu/3m5tyjv3ssc2p"&gt;here&lt;/a&gt;, the new URL is &lt;a href="https://lab.mackuba.eu"&gt;https://lab.mackuba.eu&lt;/a&gt; (I&amp;rsquo;ll add a redirect from &amp;lsquo;journal&amp;rsquo; later).&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Just a quick update, if you&amp;rsquo;re following this blog via RSS: I&amp;rsquo;ve started a separate &amp;ldquo;journal&amp;rdquo; blog on micro.blog: &lt;a href="https://journal.mackuba.eu"&gt;journal.mackuba.eu&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://micro.blog"&gt;Micro.blog&lt;/a&gt; is an interesting service: it&amp;rsquo;s a one-man indie business that&amp;rsquo;s sort of a hybrid between a blogging platform and a microblogging social network. You can write anything between full-size blog posts and tweet-sized single messages, and you can cross-post them to Bluesky, Mastodon etc. You can also follow people from the community that&amp;rsquo;s formed there and reply to them, all in the form of those mini-blogposts (there are no likes or retweets though). The idea, as I&amp;nbsp;understand, is to use a network of blogs to build a social network that uses the web itself as the foundation.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not really planning to use it in this social network mode, since I&amp;rsquo;m pretty happy now posting on Bluesky and to limited degree on Mastodon (I&amp;rsquo;ve completely stopped posting on Twitter at this point, since last autumn, when Elon started openly supporting Trump). I&amp;rsquo;m also not completely sold on this &amp;ldquo;web as a social network&amp;rdquo; idea. And I&amp;nbsp;don&amp;rsquo;t intend it to replace this blog here either – I&amp;nbsp;will still be (very) occasionally posting those super long articles here like &lt;a href="/2014/10/06/a-guide-to-nsbutton-styles/"&gt;the one about NSButtons&lt;/a&gt; or &lt;a href="/2024/02/21/bluesky-guide/"&gt;the guide to Bluesky&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But I&amp;rsquo;ve felt the need for a while to have a place to post something in between those – not full blog, and not a micro blog, but a &amp;ldquo;mediumblog&amp;rdquo; so to say (not to be confused with a Medium blog) – like this post, for example. Something where I&amp;nbsp;can sometimes post my thoughts more easily, when I&amp;nbsp;want to write something that doesn&amp;rsquo;t really fit in a few skeets/toots, with less effort required to start and finish it. This seems like it could work for that.&lt;/p&gt;

&lt;p&gt;It&amp;rsquo;s also nice that it&amp;rsquo;s supposed to sync replies from Bluesky/Mastodon under the posted link back to the blog page as comments below (I&amp;rsquo;ll try to implement the same thing here). I&amp;nbsp;also have it configured with my own domain, so I&amp;nbsp;can possibly migrate it to something self-hosted like Jekyll or Hugo at some point, keeping all the links and content.&lt;/p&gt;

&lt;p&gt;For now, I&amp;rsquo;ve posted two updates about what I&amp;rsquo;ve been working on recently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;about &lt;a href="https://journal.mackuba.eu/2025/01/16/postgress-progress/"&gt;tuning a Postgres database&lt;/a&gt; to which I&amp;rsquo;m trying to migrate my Bluesky feeds service&lt;/li&gt;
&lt;li&gt;and a &lt;a href="https://journal.mackuba.eu/2025/02/10/year-review/"&gt;review of all I&amp;rsquo;ve done in 2024&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;I&amp;nbsp;don&amp;rsquo;t know how often I&amp;nbsp;will end up posting there, I&amp;nbsp;don&amp;rsquo;t want to pressure myself, just to have a place to post when I&amp;nbsp;have a need. So if you&amp;rsquo;re curious, follow me there &lt;a href="https://journal.mackuba.eu/feed.xml"&gt;via RSS&lt;/a&gt; (or on &lt;a href="https://bsky.app/profile/mackuba.eu"&gt;Bluesky&lt;/a&gt; or &lt;a href="https://martianbase.net/@mackuba"&gt;Mastodon&lt;/a&gt;).&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://mackuba.eu/2024/03/27/march-projects-update/</id>
    <title>March 2024 projects update</title>
    <published>2024-03-27T01:14:35Z</published>
    <updated>2024-03-27T01:14:35Z</updated>
    <link href="https://mackuba.eu/2024/03/27/march-projects-update/"/>
    <content type="html">&lt;p&gt;I&amp;rsquo;ve been still pretty busy with various Bluesky- and social-related projects recently, so here&amp;rsquo;s a small update on what I&amp;rsquo;ve been working on since my &lt;a href="https://mackuba.eu/2023/11/09/year-of-social-media-coding/"&gt;November post&lt;/a&gt;, if you&amp;rsquo;re interested:&lt;/p&gt;

&lt;h3&gt;Skythread – quote &amp;amp; hashtag search&lt;/h3&gt;

&lt;p&gt;I&amp;nbsp;was missing one useful feature that&amp;rsquo;s still not available on Bluesky: being able to see the number of quote posts a post has received and looking up the list of those quote posts. The Bluesky AppView doesn&amp;rsquo;t currently collect and expose this info, so it&amp;rsquo;s not a simple matter of calling the API. But since everything&amp;rsquo;s open, anyone can build a service that does this, they just need to collect the data themselves.&lt;/p&gt;

&lt;p&gt;Since I&amp;rsquo;m already recording all recent posts in a database for the purposes of feeds and other tools, I&amp;nbsp;figured I&amp;nbsp;could just add an indexed &lt;code&gt;quote_id&lt;/code&gt; column and set it to reference the source post on all incoming posts that are quotes, and later look up the quotes using that field.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blue.mackuba.eu/skythread/"&gt;Skythread&lt;/a&gt;, my thread reading tool, seemed like a good place to add a UI&amp;nbsp;for this. When you look up a thread there, it now makes a call to a private endpoint on my server which returns the number of quotes of the root post, and if there are any, it shows an appropriate link below the post. The link leads you to another page that lists the quotes in a reverse-chronological order, &lt;a href="https://blue.mackuba.eu/skythread/?quotes=https://bsky.app/profile/bsky.app/post/3klzrudt4uk2z"&gt;like this&lt;/a&gt; (it doesn&amp;rsquo;t currently do pagination though). You can open that page directly by appending the &lt;code&gt;bsky.app&lt;/code&gt; URL of a post after the &lt;code&gt;quotes=&lt;/code&gt; parameter here: &lt;a href="https://blue.mackuba.eu/skythread/?quotes="&gt;https://blue.mackuba.eu/skythread/?quotes=&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the same way, I&amp;nbsp;also indexed &lt;a href="https://blue.mackuba.eu/skythread/?hash=wwdc"&gt;posts including hashtags&lt;/a&gt;, since hashtags were being written into post records since the autumn, but it wasn&amp;rsquo;t possible to search for them in the app. However, this has now been added to the Bluesky app and search service, so you don&amp;rsquo;t need to use Skythread for that. I&amp;nbsp;hope that the quote search also won&amp;rsquo;t be needed for much longer&amp;nbsp;:)&lt;/p&gt;

&lt;p class="image"&gt;&lt;a href="/images/posts/social-march24/skythread-quotes.jpg"&gt;&lt;img alt="Quotes link below a post" src="https://mackuba.eu/images/posts/social-march24/skythread-quotes.jpg?1771213129" width="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Handles directory&lt;/h3&gt;

&lt;p&gt;One very cool feature of Bluesky is that you can verify the authenticity of your account by yourself, by proving that you own the domain name that you&amp;rsquo;ve used as your handle. So for official accounts like &lt;a href="https://bsky.app/profile/nytimes.com"&gt;The New York Times&lt;/a&gt;, &lt;a href="https://bsky.app/profile/washingtonpost.com"&gt;The Washington Post&lt;/a&gt;, or &lt;a href="https://bsky.app/profile/ocasio-cortez.house.gov"&gt;Alexandria Ocasio-Cortez&lt;/a&gt;, it&amp;rsquo;s enough if they just set their handle to their main website domain (or a subdomain of &lt;a href="https://www.house.gov"&gt;house.gov&lt;/a&gt; in AOC&amp;rsquo;s case) to prove they&amp;rsquo;re legit – they don&amp;rsquo;t need to apply anywhere to get a blue or gold tick on their profile.&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;was thinking one day that it would be nice to see how many e.g. &lt;code&gt;.gov&lt;/code&gt; handles there are and notice easily when new ones show up. So I&amp;nbsp;grabbed a list of all custom handes from the &lt;a href="https://plc.directory"&gt;plc.directory&lt;/a&gt; and started recording new and updated ones from the firehose.&lt;/p&gt;

&lt;p&gt;In the end, I&amp;nbsp;decided to build a whole &lt;a href="https://blue.mackuba.eu/directory/"&gt;catalog of all custom handles&lt;/a&gt;, grouped by TLD, and show which TLDs are the most popular. At first I&amp;nbsp;only included the &amp;ldquo;traditional&amp;rdquo; main TLDs and country domains, but a lot of people liked it and I&amp;nbsp;got a lot of requests to also include domains like &lt;code&gt;.art&lt;/code&gt;, &lt;code&gt;.blue&lt;/code&gt;, &lt;code&gt;.xyz&lt;/code&gt; and so on, so in the next update I&amp;rsquo;ve added all other domains too. (I&amp;nbsp;gave it an old-school tables-based design as a homage to the old &amp;ldquo;web directory&amp;rdquo; websites like &lt;a href="https://web.archive.org/web/20141122194515/https://dir.yahoo.com/"&gt;Yahoo Directory&lt;/a&gt; 😉)&lt;/p&gt;

&lt;p&gt;Apart from handles, the website now also tracks &lt;a href="https://blue.mackuba.eu/directory/pdses"&gt;third party PDS servers&lt;/a&gt; that started to show up after the &lt;a href="https://bsky.social/about/blog/02-22-2024-open-social-web"&gt;federation launch in February&lt;/a&gt;.&lt;/p&gt;

&lt;p class="image"&gt;&lt;a href="/images/posts/social-march24/directory.jpg"&gt;&lt;img alt="Bluesky handles directory screenshot" src="https://mackuba.eu/images/posts/social-march24/directory.jpg?1771213129" width="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Bluesky activity charts&lt;/h3&gt;

&lt;p&gt;I&amp;rsquo;ve also made a page that shows some charts tracking &lt;a href="https://blue.mackuba.eu/stats/"&gt;Bluesky user activity&lt;/a&gt; – the number of daily posts and unique users that have posted in a given day. The activity has been gradually falling since October until February, then there was a huge spike when Bluesky &lt;a href="https://bsky.social/about/blog/02-06-2024-join-bluesky"&gt;opened up for registrations&lt;/a&gt; without an invite (when Japan &lt;a href="https://bsky.app/profile/mackuba.eu/post/3kkubfjxudp2d"&gt;suddenly took over&lt;/a&gt;), and then it&amp;rsquo;s been falling down again since then (currently around the level of the October top).&lt;/p&gt;

&lt;p&gt;You can also see some other interesting stats on &lt;a href="https://bsky.jazco.dev/stats"&gt;Jaz&amp;rsquo;s page&lt;/a&gt; and &lt;a href="https://bskycharts.edavis.dev/edavis.dev/bskycharts.edavis.dev/index.html"&gt;Eric Davis&amp;rsquo;s Munin charts&lt;/a&gt;, especially the one tracking &lt;a href="https://bskycharts.edavis.dev/edavis.dev/bskycharts.edavis.dev/bsky_users_total.html"&gt;daily/weekly/monthly active user count&lt;/a&gt;. (I&amp;nbsp;also have a few more ideas for what to add to my charts.)&lt;/p&gt;

&lt;p class="image"&gt;&lt;a href="/images/posts/social-march24/post-stats.jpg"&gt;&lt;img alt="Bluesky daily post stats chart" src="https://mackuba.eu/images/posts/social-march24/post-stats.jpg?1771213129" width="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;DIDKit&lt;/h3&gt;

&lt;p&gt;In the last few weeks, I&amp;rsquo;ve been updating the code that tracks custom handles again to adapt to some protocol changes. The &lt;code&gt;#handle&lt;/code&gt; event in the firehose, which included handle info on every handle change, is now deprecated and &lt;a href="https://github.com/bluesky-social/atproto/discussions/2220"&gt;being replaced&lt;/a&gt; with a new &lt;code&gt;#identity&lt;/code&gt; event, which only tells you to go fetch the account info from the source again (source being usually &lt;a href="https://plc.directory"&gt;plc.directory&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;At the same time, I&amp;nbsp;also implemented validation of custom handles – clients and services that display handles are supposed to verify the handle reference in both directions themselves, because some accounts may have handles in the PLC registry assigned to domains that they don&amp;rsquo;t actually own or which don&amp;rsquo;t exist (the Bluesky official app shows such accounts with an &amp;ldquo;⚠ Invalid Handle&amp;rdquo; label, which you&amp;rsquo;ve probably seen before). For example, the handles directory page initially listed an &lt;code&gt;amongus.gov&lt;/code&gt; account under &lt;code&gt;.gov&lt;/code&gt; TLD, which was loaded from plc.directory, but is not in fact a real domain.&lt;/p&gt;

&lt;p&gt; This should ideally be done by not relying on Bluesky servers, and instead checking the DNS TXT entry and the &lt;code&gt;.well-known&lt;/code&gt; URL of a given domain manually. There&amp;rsquo;s a bunch of pretty generic logic there that will be needed in most projects that need to convert between DIDs and handles, so I&amp;nbsp;extracted it to another Ruby gem named &lt;a href="https://github.com/mackuba/didkit"&gt;DIDKit&lt;/a&gt;, which lets you do things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;get the DID of an account with a given handle&lt;/li&gt;
&lt;li&gt;load the DID JSON document, which includes info like assigned handle(s) or hosting PDS server&lt;/li&gt;
&lt;li&gt;check if any of the assigned handles from the document resolve back to the same DID&lt;/li&gt;
&lt;li&gt;fetch all updates to all DIDs in batches from the PLC directory&lt;/li&gt;
&lt;/ul&gt;



        &lt;a class="github-card" href="https://github.com/mackuba/didkit" target="_blank"&gt;
          &lt;h2&gt;&lt;span class="author"&gt;mackuba&lt;/span&gt; ∕ &lt;span class="repo"&gt;didkit&lt;/span&gt;&lt;/h2&gt;
          &lt;p class="description"&gt;A library for handling DID identifiers used in Bluesky AT Protocol&lt;/p&gt;
          &lt;img src="https://mackuba.eu/images/github-mark.png?1771213129" class="gh-logo"&gt;
        &lt;/a&gt;
      

&lt;h3&gt;Skyfall&lt;/h3&gt;

&lt;p&gt;I&amp;rsquo;ve also been making some minor updates to my &lt;a href="https://github.com/mackuba/skyfall"&gt;Skyfall&lt;/a&gt; library for streaming data from the Bluesky relay firehose.&lt;/p&gt;

&lt;p&gt;One thing I&amp;rsquo;ve been trying to fix is a rare but annoying issue with the websocket connection getting stuck. From time to time, it manages to get into a state where no data is coming, but the connection doesn&amp;rsquo;t time out and just waits for new packets for hours, until I&amp;nbsp;notice it and restart it. It isn&amp;rsquo;t only happening to me, others have mentioned it too (and not only in Ruby code); but it happens rarely enough that it&amp;rsquo;s really hard to debug.&lt;/p&gt;

&lt;p&gt;My proposed fix is adding a &lt;a href="https://github.com/mackuba/skyfall/commit/5d485ae61eccc16a138c509c3a4d643b7586e6b0"&gt;&amp;ldquo;heartbeat&amp;rdquo; timer&lt;/a&gt;, which runs with some interval like every 30 seconds, and checks if there have been any new packets in some period of time; if there haven&amp;rsquo;t been any in a while, then it will forcefully restart the connection. (This isn&amp;rsquo;t included in the latest release yet, I&amp;rsquo;m waiting for it to get triggered a few times first.)&lt;/p&gt;

&lt;p&gt;Another thing I&amp;rsquo;ve added is being able to connect to a new kind of firehose exposed by &amp;ldquo;labellers&amp;rdquo; a.k.a. moderation services. Bluesky has released this new important piece of the federated architecture &lt;a href="https://docs.bsky.app/blog/blueskys-moderation-architecture"&gt;earlier this month&lt;/a&gt; – third party developers and communities can now set up independent moderation services, which manually or automatically add various &amp;ldquo;labels&amp;rdquo; to accounts or specific posts, flagging them e.g. as &amp;ldquo;racism&amp;rdquo; or &amp;ldquo;disinformation&amp;rdquo;. Anyone can subscribe to any labellers they choose, and they&amp;rsquo;ll see the labels from those selected services shown in the app. The new firehose (the &lt;code&gt;subscribeLabels&lt;/code&gt; endpoint) allows you to connect to a specific labeller and stream all new labels that it&amp;rsquo;s adding.&lt;/p&gt;

&lt;p&gt;I&amp;rsquo;m also tracking all new registered labeller services and &lt;a href="https://blue.mackuba.eu/labellers/"&gt;keeping a list here&lt;/a&gt; (it&amp;rsquo;s not curated, just a dump from a database table, so it also includes various test servers etc.).&lt;/p&gt;


        &lt;a class="github-card" href="https://github.com/mackuba/skyfall" target="_blank"&gt;
          &lt;h2&gt;&lt;span class="author"&gt;mackuba&lt;/span&gt; ∕ &lt;span class="repo"&gt;skyfall&lt;/span&gt;&lt;/h2&gt;
          &lt;p class="description"&gt;A Ruby gem for streaming data from the Bluesky/AtProto firehose&lt;/p&gt;
          &lt;img src="https://mackuba.eu/images/github-mark.png?1771213129" class="gh-logo"&gt;
        &lt;/a&gt;
      

&lt;h3&gt;The &amp;ldquo;Bluesky guide&amp;rdquo;&lt;/h3&gt;

&lt;p&gt;Last month I&amp;nbsp;wrote a long blog post titled &lt;a href="https://mackuba.eu/2024/02/21/bluesky-guide/"&gt;&amp;ldquo;Complete guide to Bluesky&amp;rdquo;&lt;/a&gt;, where I&amp;nbsp;included various info and tips for beginners about Bluesky history, available apps, handles, custom feeds, privacy, or currently missing features. I&amp;rsquo;m now updating it every time Bluesky releases another big feature&amp;nbsp;:) Check it out if you&amp;rsquo;ve missed it.&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;have ideas for a few more Bluesky introduction posts with a developer focus – a general intro to the protocol and architecture, and about working with the XRPC API&amp;nbsp;and the firehose. I&amp;nbsp;hope I&amp;rsquo;ll be able to find time for that in the next few months.&lt;/p&gt;

&lt;h3&gt;Tootify – cross-posting to Mastodon&lt;/h3&gt;

&lt;p&gt;I&amp;nbsp;still want to finish my &lt;a href="https://bsky.app/profile/mackuba.eu/post/3k3y7lxorqd24"&gt;Mac app for cross-posting&lt;/a&gt; to Twitter, Mastodon and Bluesky one day, but it&amp;rsquo;s a lot of work and I&amp;rsquo;ve got too many different things in progress at the same time, so it&amp;rsquo;s moving at a glacial pace… In the meantime, I&amp;nbsp;started thinking if I&amp;nbsp;could maybe quickly build something much simpler that also does the job. I&amp;rsquo;ve been mainly hanging out on Bluesky in recent months and posting on Mastodon only occasionally, because having to copy-paste things from one tab to another is annoying, especially if images and alt text are involved. But the folks I&amp;nbsp;know from Twitter still mostly follow me on Twitter and Mastodon only and aren&amp;rsquo;t coming to Bluesky.&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;also didn&amp;rsquo;t want to simply copy every single post from here to there, because a lot of things I&amp;nbsp;post on Bluesky are specifically about Bluesky stuff, so it doesn&amp;rsquo;t always make sense to post them to Mastodon – I&amp;nbsp;only want some selected ones to be copied. But at the same time, I&amp;nbsp;wanted to minimize the amount of friction this would add.&lt;/p&gt;

&lt;p&gt;So the idea I&amp;nbsp;had one night was that I&amp;nbsp;could mark the Bluesky posts to be copied to Mastodon by simply &amp;ldquo;liking&amp;rdquo; my own posts that I&amp;nbsp;want copied; a service or a cron job would then periodically look at the list of my recent likes, and when it notices one made on my own post, it would copy that post to Mastodon (and remove the like).&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;managed to build it in about a day and a half, complete with image support with alt text and copying of quote-posts as posts with plain links. It&amp;rsquo;s now running happily on a &lt;a href="https://bsky.app/profile/mackuba.eu/post/3jx6qhmaa3t2e"&gt;Raspberry Pi&lt;/a&gt; on my local network 😎&lt;/p&gt;

&lt;p&gt;The code is &lt;a href="https://github.com/mackuba/tootify"&gt;published here&lt;/a&gt;, if you&amp;rsquo;re interested – but it&amp;rsquo;s a bit of a proof of concept at the moment, just enough to make it work for myself, so it&amp;rsquo;s probably not very user-friendly. But maybe I&amp;rsquo;ll build it up into something bigger if people find it useful. (Just to clarify, this is meant to be a one-way sync by design – syncing in the other direction would be harder for various reasons, e.g. because of the complex &amp;ldquo;facets&amp;rdquo; system that Bluesky uses for post record data, and because Mastodon&amp;rsquo;s post length limit is higher than on Bluesky.)&lt;/p&gt;


        &lt;a class="github-card" href="https://github.com/mackuba/tootify" target="_blank"&gt;
          &lt;h2&gt;&lt;span class="author"&gt;mackuba&lt;/span&gt; ∕ &lt;span class="repo"&gt;tootify&lt;/span&gt;&lt;/h2&gt;
          &lt;p class="description"&gt;Toot toooooooot&lt;/p&gt;
          &lt;img src="https://mackuba.eu/images/github-mark.png?1771213129" class="gh-logo"&gt;
        &lt;/a&gt;
      

&lt;h3&gt;And One More Thing&amp;nbsp;;)&lt;/h3&gt;

&lt;p&gt;Paul Frazee, Bluesky&amp;rsquo;s lead dev, has a lovely cat named Kit and often posts photos of her. I&amp;rsquo;m a big fan of Kit, so I&amp;nbsp;made a feed named the &lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/kit"&gt;Kit Feed&lt;/a&gt;, which only includes posts with these photos 🙂 Like and subscribe! 🐱&lt;/p&gt;

&lt;p class="image"&gt;&lt;a href="/images/posts/social-march24/paul-kit.jpg"&gt;&lt;img alt="@pfrazee.com: the feisty / sleepy cycle (attached two photos of Kit lying on a couch)" src="https://mackuba.eu/images/posts/social-march24/paul-kit.jpg?1771213129" width="595"&gt;&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://mackuba.eu/2024/02/21/bluesky-guide/</id>
    <title>A complete guide to Bluesky 🦋</title>
    <published>2024-02-21T18:05:12Z</published>
    <updated>2024-02-21T18:05:12Z</updated>
    <link href="https://mackuba.eu/2024/02/21/bluesky-guide/"/>
    <content type="html">

&lt;div class="hide-in-intro"&gt;
  &lt;p&gt;&lt;i&gt;(Last update: &lt;a href="#changelog"&gt;18 Nov 2025&lt;/a&gt;.)&lt;/i&gt;&lt;/p&gt;
&lt;/div&gt;


&lt;p&gt;For the past year and a half, I&amp;rsquo;ve been a pretty active user of Bluesky. I&amp;nbsp;enjoy it a lot, and I&amp;rsquo;ve managed to learn a lot about how it works, what works well and what doesn&amp;rsquo;t, and also what&amp;rsquo;s likely coming next.&lt;/p&gt;

&lt;p&gt;I&amp;rsquo;ve decided to write down some of the tips &amp;amp; tricks that I&amp;nbsp;often give to friends when I&amp;nbsp;invite them there, or the advice and answers that I&amp;nbsp;sometimes give to people that I&amp;nbsp;find in some feed asking about things.&lt;/p&gt;

&lt;p&gt;This of course got much longer than I&amp;nbsp;planned 😅 so if only have a moment, here’s a TLDR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;there are official &lt;a href="https://apps.apple.com/us/app/bluesky-social/id6444370199"&gt;iOS&lt;/a&gt; and &lt;a href="https://play.google.com/store/apps/details?id=xyz.blueskyweb.app"&gt;Android&lt;/a&gt; apps, but you can also use &lt;a href="https://bsky.app"&gt;bsky.app&lt;/a&gt; in the browser, or try e.g. &lt;a href="https://apps.apple.com/us/app/skeets-for-bluesky/id6466340923"&gt;Skeets&lt;/a&gt; (iOS), &lt;a href="https://play.google.com/store/apps/details?id=com.gmail.mfnboer.skywalker"&gt;Skywalker&lt;/a&gt; (Android), or &lt;a href="https://deck.blue"&gt;deck.blue&lt;/a&gt; (multi-column webapp)&lt;/li&gt;
&lt;li&gt;tweak the algorithm in your &amp;ldquo;Discover&amp;rdquo; feed by opening the &amp;ldquo;…&amp;rdquo; context menu on a post in that feed and selecting &amp;ldquo;Show more like this&amp;rdquo; or &amp;ldquo;Show less like this&amp;rdquo;; or, if you prefer a classic chronological feed, you can set the Following feed as your main feed and remove Discover&lt;/li&gt;
&lt;li&gt;if your timeline feels empty, you can find some people to follow through &lt;a href="https://blueskydirectory.com/starter-packs"&gt;&amp;ldquo;starter packs&amp;rdquo;&lt;/a&gt;, or you can go to the “Feeds” tab, scroll down to the &amp;ldquo;Discover New Feeds” section and look for some feeds on the topics that interest you (on the top list or in the search); follow these feeds, and then if you find some interesting people posting in those feeds, follow them too. You can also search for feeds on &lt;a href="https://goodfeeds.co"&gt;goodfeeds.co&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;don’t be afraid to interact with people, repost good posts, like good comments, comment in threads and so on &amp;ndash; that’s how you make friends! (but be nice&amp;nbsp;:)&lt;/li&gt;
&lt;li&gt;if you see too much NSFW stuff, look for “Content filters” settings in the Moderation tab in Settings&lt;/li&gt;
&lt;li&gt;everything you post here is very public, so don’t share anything too private 😏&lt;/li&gt;
&lt;li&gt;if you own some cool domain name like “&lt;a href="https://taylorswift.com"&gt;taylorswift.com&lt;/a&gt;”, you can set it as your handle&lt;/li&gt;
&lt;li&gt;bookmarks, trends, thread composer, pinned posts, videos, GIFs and DMs (simple version) are now available&lt;/li&gt;
&lt;li&gt;2FA and more DM stuff are coming, post editing is planned; some version of private posts shared to limited audience is probably coming at some point, but not in near future&lt;/li&gt;
&lt;li&gt;Jack Dorsey has nothing to do with Bluesky anymore&amp;nbsp;;)&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;And now the long version:&lt;/p&gt;
&lt;div class="toc"&gt;&lt;ol&gt;
&lt;li&gt;&lt;a href="#what-is-bluesky"&gt;What is Bluesky?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#apps"&gt;Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#feeds"&gt;Feeds &amp;amp; algorithms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#safety"&gt;Safety &amp;amp; moderation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#privacy"&gt;Privacy of your data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#security"&gt;Account security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#handles"&gt;Handles &amp;amp; IDs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#terms"&gt;How are things called here?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#federation"&gt;What is this federation thing?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#search"&gt;Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#hashtags"&gt;Hashtags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#video"&gt;Video &amp;amp; GIFs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#starter-packs"&gt;Starter packs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#trends"&gt;Trending topics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bookmarks"&gt;Bookmarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#settings"&gt;Settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#missing"&gt;Missing features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#tools"&gt;Other tools&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;


&lt;hr /&gt;

&lt;p id="what-is-bluesky"&gt;&lt;/p&gt;

&lt;h2&gt;What is Bluesky?&lt;/h2&gt;

&lt;p&gt;(A bit of history, skip if you’re not interested&amp;nbsp;:)&lt;/p&gt;

&lt;p&gt;Bluesky is a project started originally by Twitter (now an independent company), whose goal is to create a decentralized Twitter-like social network, or more generally, a platform for building various decentralized social networks.&lt;/p&gt;

&lt;p&gt;The project was &lt;a href="https://www.theverge.com/2019/12/11/21010856/twitter-jack-dorsey-bluesky-decentralized-social-network-research-moderation"&gt;started in Dec 2019&lt;/a&gt; by &lt;a href="https://twitter.com/jack/status/1204766078468911106"&gt;Jack Dorsey&lt;/a&gt;, former Twitter CEO. The basic idea was to design a protocol on which you could build something that would work like Twitter, but which would not be under the control of a single company that makes unilateral decisions about everything on it. It would be more like web and email, which are open standards that anyone can build on &amp;ndash; any company can set up an email service or write an email app, and anyone can sign up for an account and start sending emails. There’s no one central authority on the Internet that can ban you from email altogether.&lt;/p&gt;

&lt;p&gt;Such network would consist of many servers owned by different companies and people connecting together, and the idea was that eventually, Twitter itself could become a part of that network, as just one of its elements.&lt;/p&gt;

&lt;p&gt;(If this all sounds a lot like the Mastodon social network, or “the Fediverse”, then you’re right &amp;ndash; there are a lot of similarities between these two. However, Bluesky is built on a completely different system they’ve designed from scratch, called the AT Protocol or ATProto. They’re hoping that this will let them build some things better than they are done in Mastodon, making the network less confusing, more useful and more user-friendly. Bluesky does not directly connect with Mastodon servers and apps, although there are some unofficial ways to connect the two worlds.)&lt;/p&gt;

&lt;p&gt;After an initial research phase, in 2021 a team was chosen to build the platform and the Bluesky company was formally created. A woman named Jay Graber, formerly a developer at Zcash, &lt;a href="https://www.theverge.com/2021/8/16/22627435/twitter-bluesky-lead-jay-graber-decentralized-social-web"&gt;was chosen as the CEO&lt;/a&gt;. Thankfully, Jay had the foresight at that point to insist that they&amp;rsquo;d set it up as an independent company, which was funded, but not controlled by Twitter. Had they not, it almost certainly would have been shut down last year after Elon&amp;rsquo;s Twitter takeover.&lt;/p&gt;

&lt;p&gt;The team has been working on designing and building the pieces of the system throughout 2022, and in February 2023 they&amp;rsquo;ve &lt;a href="https://techcrunch.com/2023/02/28/jack-dorsey-backed-twitter-alternative-bluesky-hits-the-app-store-as-an-invite-only-app/"&gt;launched&lt;/a&gt; a very early and rough beta and started slowly letting in some users who wanted to try it out and have signed up on a waitlist. However, the whole Elon thing happened in the meantime and that was a moment when everyone was looking for a Twitter alternative, so the interest has wildly exceeded expectations and they weren&amp;rsquo;t ready yet to take in everyone.&lt;/p&gt;

&lt;p&gt;Since then, the user base has been gradually growing, with people being let in from the waitlist and existing users inviting their friends using invite codes. Meanwhile, the team had to speed some things up to adapt to the new situation and has been working hard on adding the most important features, and building up the backend to allow for more and more traffic. Finally, almost a year later, in February 2024 &lt;a href="https://bsky.social/about/blog/02-06-2024-join-bluesky"&gt;Bluesky has opened up for registrations from everyone&lt;/a&gt;.&lt;/p&gt;

&lt;p id="company"&gt;&lt;/p&gt;

&lt;h3&gt;The Bluesky company&lt;/h3&gt;

&lt;p&gt;Bluesky is still a fairly small team at the moment. The dev team is probably something like a dozen people altogether, and that’s for the frontend, backend, protocol, servers and so on. So they just can’t add new features as fast as they’d like to, but they’re doing what they can.&lt;/p&gt;

&lt;p&gt;The team members interact with people on the platform all the time, answering questions and just having fun in general. They’re also building almost everything in public &amp;ndash; &lt;a href="https://github.com/bluesky-social/"&gt;the source code&lt;/a&gt; of the app and servers is available on GitHub, so we can track in real time what they’re working on next, report bugs and sometimes submit code with some new features they can merge in.&lt;/p&gt;

&lt;p&gt;The company is set up as a “&lt;a href="https://en.wikipedia.org/wiki/Benefit_corporation"&gt;public benefit corporation&lt;/a&gt;”, which basically means (in my non-US layman understanding) that it is a business and it&amp;rsquo;s meant to make profit, but that profit is not it&amp;rsquo;s only and main goal. It can and should have other, more noble goals that benefit the public, as the term implies, in this case: creating a protocol for decentralized social apps that everyone can build on.&lt;/p&gt;

&lt;p&gt;Bluesky isn&amp;rsquo;t really making any money at the moment (other than a small &lt;a href="https://bsky.social/about/blog/7-05-2023-namecheap"&gt;domain reselling service&lt;/a&gt;). The general plan is that, rather than the standard route of eventually putting ads everywhere and selling user data, they will resist &lt;a href="https://www.wired.com/story/bluesky-ceo-jay-graber-wont-enshittify-ads/"&gt;“enshittifying” the platform&lt;/a&gt; and will instead add some optional &lt;a href="https://bsky.social/about/blog/10-24-2024-series-a"&gt;premium plans and services&lt;/a&gt;, but in such a way that wouldn&amp;rsquo;t change the social dynamics of the platform:&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;Bluesky will always be free to use — we believe that information and conversation should be easily accessible, not locked down. We won’t uprank accounts simply because they’re subscribing to a paid tier.&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;The first planned step is a &amp;ldquo;Discord-like&amp;rdquo; subscription, which was planned for the end of 2024 / beginning of 2025, but was delayed because of the huge influx of new users that happened in November. More long term, they were also talking about coming up with some ways of letting creators and devs on the platform earn some money from their followers, with Bluesky taking some commision from the payments.&lt;/p&gt;

&lt;p&gt;In any case, they’re explicitly building the network to be resilient even in the unlikely scenario that they themselves “turn evil” in the future &amp;ndash; the network is meant to be “billionaire-proof”, impossible to completely take over by one guy with too much money. To quote the &lt;a href="https://bsky.app/profile/did:plc:ragtjsm2j2vknwkz3zp4oxrd/post/3jypici6ihm2m"&gt;lead dev&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;&amp;ldquo;Our culture doc includes the phrase “&lt;strong&gt;The company is a future adversary&lt;/strong&gt;” to remind us that we won’t always be at the helm – or at our best – and that we should always give people a safe exit from our company. It’s weird at times to frame our priorities as protecting users from us, but that’s exactly what we’re trying to do.&lt;/p&gt;

&lt;p&gt;The Bluesky team is made up of users. None of us come from big tech companies. We all came together because we were frustrated by the experience of feeling helpless about how our online communities were being run. We don’t want to give that same feeling to other people now that we’re the builders.&lt;/p&gt;

&lt;p&gt;When we build an open protocol, we’re giving out the building blocks. We want to start from the premise that we’re not always right or best, that when we are right or best then it might not last, and that communities should be empowered to build away from us. Sometimes this can all feel very intangible and abstract, and for the average user the goal is to just feel like a good &amp;amp; usable network. But this is one big reason why we put all the Fancy Technology under the hood.&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;Also, contrary to what you might have heard, this isn’t “Jack Dorsey’s company”. Yes, he started the whole thing, but he isn’t running the company – Jay Graber is. Jack was very little involved in the project after they started building; he basically gathered a team, gave them a lot of money and let them do their thing.&lt;/p&gt;

&lt;p&gt;In fact, he deleted his account on the platform last summer, after he was booed off it by the users, and now he’s mostly hanging out on Nostr instead. In early May 2024, it was announced that &lt;a href="https://www.theverge.com/2024/5/5/24149543/jack-dorsey-gone-bluesky-board"&gt;Jack has left the board of directors&lt;/a&gt; too – he&amp;rsquo;s been replaced by &lt;a href="https://bsky.social/about/blog/08-06-2024-board"&gt;Mike Masnick&lt;/a&gt; later. Dorsey also &lt;a href="https://bsky.app/profile/did:plc:oky5czdrnfjpqslsw2a5iclo/post/3krxdfy6koc22"&gt;doesn&amp;rsquo;t own any shares of Bluesky&lt;/a&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;p id="apps"&gt;&lt;/p&gt;

&lt;h2&gt;Apps&lt;/h2&gt;

&lt;p&gt;Bluesky has an official mobile app for &lt;a href="https://apps.apple.com/us/app/bluesky-social/id6444370199"&gt;iOS&lt;/a&gt; and &lt;a href="https://play.google.com/store/apps/details?id=xyz.blueskyweb.app"&gt;Android&lt;/a&gt;. It’s written in React Native, so it doesn’t feel fully native in all places and some system integration features take a while to be added &amp;ndash; the reason is that they’ve started with a very small team at first (I&amp;nbsp;think initially just one guy did all the frontend), so the only way they could do it was to build for both platforms &amp;amp; the web from one codebase.&lt;/p&gt;

&lt;p&gt;At this point, after various improvements made later, the mobile app is mostly ok and gets better with every update, it’s also obviously the most feature-complete one. One issue is that it doesn’t support iPad yet.&lt;/p&gt;

&lt;p&gt;There is of course also a web interface, at &lt;a href="https://bsky.app"&gt;bsky.app&lt;/a&gt;, which is pretty good &amp;ndash; this is the main UI&amp;nbsp;that I&amp;nbsp;use Bluesky with (also works on the iPad). At the moment it’s mostly meant to be used while logged in, although some pages like posts/threads and profile views can be viewed unauthenticated (the search doesn&amp;rsquo;t currently work when logged out for performance reasons).&lt;/p&gt;

&lt;p&gt;But there is also a small but enthusiastic group of third party developers building various apps, tools and experimenting with the protocol. They’ve built several independent apps, &lt;a href="https://sdk.blue"&gt;libraries to access the API&lt;/a&gt; in various languages, and they often manage to build various new features in their apps before the Bluesky team gets around to doing that in official apps.&lt;/p&gt;

&lt;p&gt;Here&amp;rsquo;s a few of these apps (in various stages of development):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://graysky.app"&gt;Graysky&lt;/a&gt; &amp;ndash; a mobile app, first full-featured third party app, although its author was hired by Bluesky later, so it hasn&amp;rsquo;t been updated much lately&lt;/li&gt;
&lt;li&gt;&lt;a href="https://deck.blue"&gt;deck.blue&lt;/a&gt; &amp;ndash; a web-based “Tweetdeck” column UI, written in Flutter (pending rewrite in React)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://skeetdeck.pages.dev"&gt;Skeetdeck&lt;/a&gt; &amp;ndash; another web-based, more lightweight “Tweetdeck”&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tokimeki.blue"&gt;Tokimeki&lt;/a&gt; &amp;ndash; a web app (+ PWA) with a column UI&amp;nbsp;from a Japanese developer&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.apple.com/us/app/skeets-for-bluesky/id6466340923"&gt;Skeets&lt;/a&gt; &amp;ndash; native iOS app with iPad support and some interesting features&lt;/li&gt;
&lt;li&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.gmail.mfnboer.skywalker"&gt;Skywalker&lt;/a&gt; &amp;ndash; a native app for Android&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:xg4h4n7w5mh3m4uoi27me4hh"&gt;Catbird&lt;/a&gt; &amp;ndash; new iOS client in beta&lt;/li&gt;
&lt;li&gt;&lt;a href="https://skyfeed.app"&gt;SkyFeed&lt;/a&gt; &amp;ndash; a web app with a column UI&amp;nbsp;that also lets you build custom feeds&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.apple.com/us/app/sorasns-for-mastodon-bluesky/id6754866904"&gt;SoraSNS&lt;/a&gt; &amp;ndash; a multi-network client for iOS&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openvibe.social"&gt;Openvibe&lt;/a&gt; &amp;ndash; another multi-network client with support for cross-posting and a unified timeline&lt;/li&gt;
&lt;li&gt;&lt;a href="https://suvam.io/dhaaga"&gt;Dhaaga&lt;/a&gt; &amp;ndash; a multi-network client for Android&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;The Ivory team (Tapbots) is also working on their own client called &lt;a href="https://tapbots.com/phoenix/"&gt;Phoenix&lt;/a&gt;, but at the moment it&amp;rsquo;s still work in progress.&lt;/p&gt;

&lt;p&gt;More recently, a new wave of client apps and services built on Bluesky/ATProto have also started appearing that are focused on either photos or videos, hoping to create a more open alternative for Instagram and TikTok:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:yj3p6w4e3dsyuxeyzv3xqut5"&gt;Bluescreen&lt;/a&gt;, &lt;a href="https://bsky.app/profile/did:plc:4adlzwqtkv4dirxjwq4c3tlm"&gt;Skylight&lt;/a&gt;, &lt;a href="https://bsky.app/profile/did:plc:cveom2iroj3mt747sd4qqnr2"&gt;Spark&lt;/a&gt;, &lt;a href="https://bsky.app/profile/did:plc:ampm6lbmujbvkv62dvihv5xy"&gt;Skyswipe&lt;/a&gt;, and &lt;a href="https://bsky.app/profile/did:plc:gxrjh3ztx5zoseyvihuadx5c"&gt;Twilight&lt;/a&gt; for video&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:bokpqlfmibo7e6borxfbfice"&gt;Pinksky&lt;/a&gt;, &lt;a href="https://bsky.app/profile/did:plc:24kqkpfy6z7avtgu3qg57vvl"&gt;Flashes&lt;/a&gt;, and &lt;a href="https://bsky.app/profile/did:plc:e7rftrdyz5e2rw4y6ocszew2"&gt;Grain&lt;/a&gt; for photos&lt;/li&gt;
&lt;/ul&gt;


&lt;hr /&gt;

&lt;p id="feeds"&gt;&lt;/p&gt;

&lt;h2&gt;Feeds &amp;amp; algorithms&lt;/h2&gt;

&lt;p&gt;Social networks generally include two kinds of feeds: a chronological one, showing all posts from the people you follow in order, and an algorithmic one, showing what the service thinks you will like (which often means though: what they think will bring them more profit). Centralized platforms generally push you towards an algorithmic feed and often make the chronological feed harder to reach (if at all). Mastodon on the other hand only includes chronological feeds and no algorithms.&lt;/p&gt;

&lt;p&gt;Bluesky has both &amp;ndash; and much, much more.&lt;/p&gt;

&lt;p&gt;When you sign up, you start with two default feeds. “Discover” is Bluesky’s main algorithmic feed &amp;ndash; it mixes some posts from the people you follow with some other posts that you might like. You can pick the option &amp;ldquo;Show more like this&amp;rdquo; or &amp;ldquo;Show less like this&amp;rdquo; from the menu on each post to give it some hints on what you like or don&amp;rsquo;t (note: if you tried it before and didn&amp;rsquo;t see any effect from these hints, there have been some fixes there very recently, June/July 2025, so try again). The devs are constantly tweaking it and asking for feedback, so it should be getting better over time.&lt;/p&gt;

&lt;p&gt;The second one, “Following”, is a classic chronological feed. Initially it had a sort of unique twist, in that it could show you replies from the people you follow made to anyone, regardless if you follow that other person, but this option was removed in &lt;a href="https://bsky.app/profile/bsky.app/post/3l2s5t6op2w2x"&gt;August 2025&lt;/a&gt;. If you miss that old mode, which was more noisy, but allowed you to meet friends of friends more easily, I&amp;nbsp;made two custom feeds that let you peek at those replies: &lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/follows-replies"&gt;Follows &amp;amp; Replies&lt;/a&gt; and &lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/replies"&gt;Only Replies&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But that’s just the tip of the iceberg. Bluesky has built a system where anyone with a server and some knowledge of coding can implement their own algorithmic feeds that they can share with everyone else. There are currently about 40 thousands custom feeds (as of Feb 2024) made by the Bluesky community that you can add to your app. And more importantly, the &amp;ldquo;Following&amp;rdquo; and &amp;ldquo;Discover&amp;rdquo; feeds are just what you start with by default – you can set any of the thousands of other feeds as your main feed, and you can even remove the two built-in feeds if you don&amp;rsquo;t like them, and leave e.g. only the &amp;ldquo;&lt;a href="https://bsky.app/profile/did:plc:q6gjnaw2blty4crticxkmujt/feed/cv:cat"&gt;Cat Pics&lt;/a&gt;&amp;rdquo; custom feed as your only feed tab. Nothing here is forced on you.&lt;/p&gt;

&lt;p&gt;The way a feed works is that it basically reads all new posts on Bluesky from a giant stream and decides which of them to keep and how to arrange them (this can be a shared feed, same for everyone, or a personalized feed that looks different to each user). Most feeds match posts by keywords &amp;ndash; these are feeds on some specific topics like Linux, food, gardening, climate change, astronomy, and so on. They usually define some sets of words, phrases, hashtags, sometimes emojis, and include all posts that contain any of these, chronologically.&lt;/p&gt;

&lt;p&gt;There are also various “top posts of the week / all time” feeds, posts using AI&amp;nbsp;models to match some specific kinds of photos like pictures of cats, frogs, or moss, or personal algorithmic feeds that show you posts selected for you according to some specific idea: posts from your mutual follows, from people who follow you, posts with photos only, posts from those of your friends who post less than others, and so on. These are all feeds built by third party developers who just had an idea and implemented it, without having to register or apply anywhere or ask anyone for permission.&lt;/p&gt;

&lt;p&gt;Some people have also built web-based user friendly UIs for building feeds, which let you &lt;a href="https://docs.bsky.app/blog/feature-skyfeed"&gt;build feeds using a web form&lt;/a&gt; and have them hosted for you, without having to write code or host it yourself, which allowed a lot of people without programming knowledge to build their own feeds. The first such tool was &lt;a href="https://skyfeed.app"&gt;SkyFeed&lt;/a&gt;, built by one German developer, which at some point ran the vast majority of all feeds on Bluesky; another newer one is &lt;a href="https://www.graze.social"&gt;Graze&lt;/a&gt;, a larger service with a bit friendlier interface, which recently &lt;a href="https://graze.leaflet.pub/3m4yjmbnyec2c"&gt;partnered with Bluesky&lt;/a&gt; to run a trending feed for the New York mayor election night.&lt;/p&gt;

&lt;p&gt;There&amp;rsquo;s a nice &lt;a href="https://web.archive.org/web/20250321051549/https://goodfeeds.co/the-guide"&gt;guide to feeds&lt;/a&gt; that describes them in more detail on the Goodfeeds site (archived), there is also &lt;a href="https://bsky.social/about/blog/7-27-2023-custom-feeds"&gt;a blog post about feeds on Bluesky&amp;rsquo;s blog&lt;/a&gt;.&lt;/p&gt;

&lt;p id="useful-feeds"&gt;&lt;/p&gt;

&lt;h3&gt;Useful feeds&lt;/h3&gt;

&lt;p&gt;There is a feed search engine integrated in the official app, in the Feeds tab (the “Discover new feeds” section). You can also search for interesting feeds on these two external sites: &lt;a href="https://goodfeeds.co"&gt;goodfeeds&lt;/a&gt; and &lt;a href="https://stats.skyfeed.me"&gt;SkyFeed Builder Feed Stats&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Some general feeds that you might find useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:q6gjnaw2blty4crticxkmujt/feed/whats-hot"&gt;What&amp;rsquo;s Hot&lt;/a&gt;, &lt;a href="https://bsky.app/profile/did:plc:tenurhgjptubkk5zf5qhi3og/feed/catch-up"&gt;Catch Up&lt;/a&gt; and &lt;a href="https://bsky.app/profile/did:plc:tenurhgjptubkk5zf5qhi3og/feed/catch-up-weekly"&gt;Week Peak Feed&lt;/a&gt; – general feeds with most popular posts&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:vpkhqolt662uhesyj6nxm7ys/feed/infreq"&gt;Quiet Posters&lt;/a&gt; – posts from people you follow who post less than others&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:vpkhqolt662uhesyj6nxm7ys/feed/bestoffollows"&gt;Latest From Follows&lt;/a&gt; or &lt;a href="https://bsky.app/profile/did:plc:vpkhqolt662uhesyj6nxm7ys/feed/latestmutuals"&gt;Latest From Mutuals&lt;/a&gt; – just one most recent post from each person you&amp;rsquo;re following&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur/feed/with-friends"&gt;Popular With Friends&lt;/a&gt; – recent posts liked by the people you follow&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/follows-replies"&gt;Follows &amp;amp; Replies&lt;/a&gt; and &lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/replies"&gt;Only Replies&lt;/a&gt; – my feeds that include all replies from your follows to anyone&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/only-posts"&gt;Only Posts&lt;/a&gt; – picks only top-level posts from Following without replies or reposts&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:vpkhqolt662uhesyj6nxm7ys/feed/followpics"&gt;The &amp;lsquo;Gram&lt;/a&gt; – only posts with images from the people you follow&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;And some &amp;ldquo;utility feeds&amp;rdquo;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:q6gjnaw2blty4crticxkmujt/feed/at-bangers"&gt;All-Time Bangers&lt;/a&gt; – posts with the highest number of likes on the whole platform&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:q6gjnaw2blty4crticxkmujt/feed/bangers"&gt;My Bangers&lt;/a&gt; – your own posts with the most likes&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:wzsilnxf24ehtmmc3gssy5bu/feed/quotes"&gt;Quotes&lt;/a&gt; – all quotes of your posts&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:wzsilnxf24ehtmmc3gssy5bu/feed/mentions"&gt;Mentions&lt;/a&gt; – all replies to you or mentions of you&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:q6gjnaw2blty4crticxkmujt/feed/my-pins"&gt;My Pins&lt;/a&gt; – your &amp;ldquo;bookmarks&amp;rdquo; made by commenting with the 📌 emoji&lt;/li&gt;
&lt;/ul&gt;


&lt;hr /&gt;

&lt;p id="safety"&gt;&lt;/p&gt;

&lt;h2&gt;Safety &amp;amp; moderation&lt;/h2&gt;

&lt;p&gt;Like on most other networks, you can mute someone if you find them annoying, or you can block them if you find them &lt;em&gt;really&lt;/em&gt; annoying. It&amp;rsquo;s also possible to mute words, phrases or hashtags. You can now also mute words for a specific period of time (there&amp;rsquo;s no way to mute an &lt;em&gt;account&lt;/em&gt; for a specific time yet).&lt;/p&gt;

&lt;p&gt;(Note, the blocking mechanism here is pretty aggressive, in that it also hides all previous interactions between the two users *for everyone*; so don&amp;rsquo;t be surprised if someone blocks you and your reply or quote &amp;ldquo;disappears&amp;rdquo; &amp;ndash; it wasn&amp;rsquo;t deleted, just hidden.)&lt;/p&gt;

&lt;p&gt;There&amp;rsquo;s also a number of features for controlling interactions with your posts or threads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can lock replies in a thread to only your followers, only the people you follow yourself, or some other combination, or disable them completely (also some time later after replies appear)&lt;/li&gt;
&lt;li&gt;you can choose to &amp;ldquo;hide&amp;rdquo; some specific negative replies, like on Twitter (they&amp;rsquo;re moved to a &amp;ldquo;hidden replies&amp;rdquo; section at the bottom of the thread)&lt;/li&gt;
&lt;li&gt;you can disable the option of quoting your post&lt;/li&gt;
&lt;li&gt;you can &amp;ldquo;detach&amp;rdquo; existing quote posts of your post – the quoting post stays visible, but without the embed showing your post&lt;/li&gt;
&lt;li&gt;and finally, you can temporarily &amp;ldquo;deactivate&amp;rdquo; your whole account – this makes it appear deleted to others so people won&amp;rsquo;t bother you if you need to take a break from social media&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;You can also create “moderation lists” for muting or blocking some groups of people all at once, which can be shared with others. This is meant to let various communities on Bluesky build their own “defences”, by collecting lists of people who are unpleasant or annoying in some way and letting others mute or block them in advance before they come across them.&lt;/p&gt;

&lt;p&gt;If you get added to a user list (the kind meant for following) or a starter pack that you don&amp;rsquo;t want to be on, and the author refuses to take you off it despite your request, you can solve the problem by blocking the author – you will &amp;ldquo;disappear&amp;rdquo; from their list then. For obvious reasons, this doesn&amp;rsquo;t apply to mute/block lists, since usually no one wants to be on those, but you can now report lists that are clearly hateful or made in bad faith, and &lt;a href="https://bsky.app/profile/safety.bsky.app/post/3lmwf453gbs2s"&gt;they are taking those down&lt;/a&gt;. If you don&amp;rsquo;t know what lists you&amp;rsquo;re on, you can look that up on &lt;a href="https://clearsky.app"&gt;Clearsky&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Bluesky&amp;rsquo;s own moderation seems to generally work ok, though there are some occasional problems, controversies, and periods when they get a bit overwhelmed with new waves of users; but obvious trolls, spams and scams are usually got rid of pretty quickly. They claim to have around 100 people in the moderation team now, &lt;a href="https://bsky.social/about/blog/01-17-2025-moderation-2024"&gt;as of January 2025&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The general high-level plan for moderation at Bluesky and on the AT Protocol is something they call “&lt;em&gt;&lt;a href="https://bsky.social/about/blog/4-13-2023-moderation"&gt;composable moderation&lt;/a&gt;&lt;/em&gt;” (old blog post) or “&lt;em&gt;&lt;a href="https://bsky.social/about/blog/03-12-2024-stackable-moderation"&gt;stackable moderation&lt;/a&gt;&lt;/em&gt;” (recent post). It’s an idea that moderation will have many layers &amp;ndash; from server operators including the Bluesky company, through various tools and services provided by other companies, organizations, and communities, ending with some ways to privately personalize your experience according to your personal needs.&lt;/p&gt;

&lt;p id="labellers"&gt;&lt;/p&gt;

&lt;h3&gt;Labellers&lt;/h3&gt;

&lt;p&gt;A core part of this is a feature called “&lt;em&gt;labellers&lt;/em&gt;” that they&amp;rsquo;ve released in March 2024, which are basically third-party moderation services. They work by assigning a set of labels/tags (manually or automatically) to accounts and posts – this can be because one of the labeller&amp;rsquo;s moderators has come across an offending post, or because it was detected automatically using some custom software, or because the post or account was reported to the service by a user (users can send moderation reports to any set of these services).&lt;/p&gt;

&lt;p&gt;All users on the platform can &amp;ldquo;subscribe&amp;rdquo; to one or more of these services and configure how they want these labels to affect their experience: for any label type, they can choose if the user/post marked with such label should be hidden from their view, just marked with a label, or if this kind of label should be ignored. (A labeller doesn&amp;rsquo;t have the full power of a built-in platform moderation in that it can&amp;rsquo;t just ban someone from the site and delete their account – but they can make someone &lt;em&gt;effectively&lt;/em&gt; disappear for those users who trust and agree with the given service.)&lt;/p&gt;

&lt;p&gt;Labellers will usually be specialized in some area: they could be protecting their users from things such as racism, antisemitism, or homophobia; they could be automatically detecting some unwanted behaviors like following a huge number of people quickly; marking some specific types of accounts like new accounts without an avatar, or accounts from a different network; fighting disinformation or political extremism; or they could be serving a community using a specific language or from a specific country.&lt;/p&gt;

&lt;p&gt;A simple labeller can be run by one person, but bigger ones can be managed by a whole group of people that collaborate on processing the reports (Bluesky has built an open source tool for this called &lt;a href="https://github.com/bluesky-social/ozone"&gt;Ozone&lt;/a&gt;). This system allows different communities to handle moderation in their own way independently, to make their members feel safer and have a better experience in the aspects that are important for them. And most importantly, different communities could often have somewhat conflicting or even completely opposing views on some things – and Bluesky as a company doesn&amp;rsquo;t have to try to satisfy everyone (which is impossible) or always pick a side. They also don&amp;rsquo;t necessarily have to specialize in every country, language and culture on Earth. Of course they reserve the right to take down some accounts completely, because some things and some people have to removed from the platform for everyone (e.g. things that are just illegal), but in less serious or less clear cases, they can just use labels or defer to other labellers (the Bluesky built-in moderation is now &amp;ldquo;just&amp;rdquo; another labeller among many others, using the same API, and with only &lt;em&gt;some&lt;/em&gt; special powers). They also have a few country-specific labellers now, so they can comply with government takedown orders by hiding given posts/accounts in a given country, but leaving it online for everyone else.&lt;/p&gt;

&lt;p&gt;You can read more about labellers in the guide titled
“&lt;a href="https://web.archive.org/web/20240620103516/https://from-over-the-horizon.ghost.io/bluesky-crash-course-labelers/"&gt;Bluesky Crash Course: Labelers&lt;/a&gt;” written by Kairi, who used to run one of the most popular labellers called Aegis (now defunct).&lt;/p&gt;

&lt;p&gt;There&amp;rsquo;s no easy way to search for labellers in the app yet, but I&amp;rsquo;m keeping a rough list myself on &lt;a href="https://blue.mackuba.eu/labellers/"&gt;this page&lt;/a&gt;. I&amp;nbsp;also made a “&lt;a href="https://blue.mackuba.eu/scanner/"&gt;Label Scanner&lt;/a&gt;” tool where you can find all labels assigned to a given account from any labeller.&lt;/p&gt;

&lt;p&gt;By the way, it&amp;rsquo;s fascinating how the open architecture of Bluesky allows its features to be used sometimes in completely unexpected ways. Labellers were initially designed to be used mostly for negative, unwanted things, but quite a lot of them ended up being built to let users label &lt;em&gt;themselves&lt;/em&gt; with some badges they want to show to others: there are labellers to let you assign a &lt;a href="https://bsky.app/profile/did:plc:ubt73xes4uesthuuhbqwf37d"&gt;country flag&lt;/a&gt;, &lt;a href="https://bsky.app/profile/did:plc:wkoofae5uytcm7bjncmev6n6"&gt;pronouns&lt;/a&gt;, &lt;a href="https://bsky.app/profile/did:plc:hysbs7znfgxyb4tsvetzo4sk"&gt;RPG class&lt;/a&gt;, &lt;a href="https://bsky.app/profile/did:plc:cdbp64nijvsmhuhodbuoqcwi"&gt;Zodiac sign&lt;/a&gt;, &lt;a href="https://bsky.app/profile/did:plc:yv4nuaj3jshcuh2d2ivykgiz"&gt;Hogwarts house&lt;/a&gt; and other things.&lt;/p&gt;

&lt;p&gt;Some other useful labellers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:newitj5jo3uel7o4mnf3vj2o"&gt;XBlock&lt;/a&gt;, which lets you hide screenshots of posts from other platforms like Twitter&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:i65enriuag7n5fgkopbqtkyk"&gt;Profile Labeller&lt;/a&gt;, which marks e.g. accounts created recently, without an avatar, ones that changed handle recently etc.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:mjyeurqmqjeexbgigk3yytvb"&gt;No GIFs Please&lt;/a&gt;, which is exactly what it sounds like&amp;nbsp;;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:4ugewi6aca52a62u62jccbl7"&gt;Asuka&amp;rsquo;s Anti-Transphobia Field&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;hr /&gt;

&lt;p id="privacy"&gt;&lt;/p&gt;

&lt;h2&gt;Privacy of your data&lt;/h2&gt;

&lt;p&gt;One important thing from the privacy aspect that may not be obvious at first, which you need to be aware of: the underlying protocol on which Bluesky runs is &lt;em&gt;extremely&lt;/em&gt; open. Anyone who knows how to code can write an app or tool that can read practically any data about anyone, without having to ask anyone for permission (since there’s no central authority that can require registration and payment to get API&amp;nbsp;keys like on Twitter). This is by design, because all the different pieces of the network that make it work, apps, tools and services, need to be able to access the data to provide their functionality, and we want everyone to be able to build those to keep the network decentralized and not controlled by one corporation.&lt;/p&gt;

&lt;p&gt;This has both advantages and disadvantages. For a developer, it means that the only limit is your time and imagination (and maybe API&amp;nbsp;rate limits). You can build feeds that show &lt;a href="https://bsky.app/profile/did:plc:q6gjnaw2blty4crticxkmujt/feed/cv:cat"&gt;all posts with cat photos&lt;/a&gt;, a bot that responds to the text “/honk” with a &lt;a href="https://bsky.app/profile/did:plc:jlqiqmhalnu5af3pf56jryei/post/3kdojng5j7q2i"&gt;random photo of a goose&lt;/a&gt;, implement &lt;a href="https://bsky.app/profile/did:plc:mdpndtkinvfaxtf64ubgftzs/post/3kaihxsmrxq2c"&gt;some new features&lt;/a&gt; before the Bluesky team gets around to that, count the statistics of how the &lt;a href="https://bsky.app/profile/did:plc:q6gjnaw2blty4crticxkmujt/post/3kktq7i2noz26"&gt;percentage of languages used in posts&lt;/a&gt; has changed over time, and whatever else you can think of. You don’t have any monthly quotas or paid plans.&lt;/p&gt;

&lt;p&gt;For a user though, this means that everything you do is very public &amp;ndash; kind of like it was on Twitter, just more so, because there are fewer restrictions.&lt;/p&gt;

&lt;p&gt;Specifically, all of these are &lt;strong&gt;publicly accessible&lt;/strong&gt; (even if they aren&amp;rsquo;t all displayed in the official app):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your posts&lt;/li&gt;
&lt;li&gt;your likes&lt;/li&gt;
&lt;li&gt;the photos you’ve attached to posts&lt;/li&gt;
&lt;li&gt;all the handles you&amp;rsquo;ve previously used (you can&amp;rsquo;t delete those)&lt;/li&gt;
&lt;li&gt;the list of people you follow&lt;/li&gt;
&lt;li&gt;the list of people you block (!)&lt;/li&gt;
&lt;li&gt;the user lists and moderation lists (mute/block lists) that you’ve created&lt;/li&gt;
&lt;li&gt;which moderation lists (yours or others’) you are blocking people with&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;And these things are &lt;strong&gt;private&lt;/strong&gt; and known only to you (and the apps and tools that you’ve explicitly granted access to your account):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the people you’re muting (individually or through lists)&lt;/li&gt;
&lt;li&gt;the words, phrases, hashtags etc. that you&amp;rsquo;re muting&lt;/li&gt;
&lt;li&gt;your &amp;ldquo;saved posts&amp;rdquo; / bookmarks (the new built-in ones, not the 📌 style)&lt;/li&gt;
&lt;li&gt;your selected languages and other preferences&lt;/li&gt;
&lt;li&gt;your email address, birthday and phone number&lt;/li&gt;
&lt;li&gt;who invited you and who you have invited&lt;/li&gt;
&lt;li&gt;the moderation services you&amp;rsquo;re subscribed to&lt;/li&gt;
&lt;li&gt;the accounts you&amp;rsquo;re subscribed to for post update notifications&lt;/li&gt;
&lt;li&gt;the custom feeds that you’ve saved or pinned

&lt;ul&gt;
&lt;li&gt;one caveat though: the provider of the feed knows when you are opening it&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;DMs are private between you and the person/people you&amp;rsquo;re chatting with, but they&amp;rsquo;re currently &lt;strong&gt;not end-to-end encrypted&lt;/strong&gt;, so the Bluesky team can theoretically access them, and will access them if ordered to. A fully encrypted version will come some time later. For sensitive conversations, Bluesky devs recommend that you use the DMs to just &lt;a href="https://bsky.app/profile/did:plc:44ybard66vv44zksje25o7dz/post/3lacrutxhio2h"&gt;exchange e.g. Signal usernames&lt;/a&gt; and then continue there.&lt;/p&gt;

&lt;p&gt;The difference between muting and blocking is because muting is simply a filter applied only for you &amp;ndash; nobody else needs to know that you’ve asked your app to hide some of the posts. On the other hand, blocking is inherently a two-way thing &amp;ndash; that other person, their app/server and any other pieces of the network that process their posts need to be aware of the block, so that they can prevent them from interacting with you. So the fact that the block exists needs to be publicly known.&lt;/p&gt;

&lt;p&gt;Now, what all of this can mean in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;anyone can download anyone’s posts and do various targeted or global analysis on it, track your likes and contact graph and so on&lt;/li&gt;
&lt;li&gt;if you are posting personal photos, especially NSFW photos or photos that can be geolocated, anyone can be downloading all of them automatically (though the official apps strip metadata from photos)&lt;/li&gt;
&lt;li&gt;some (any) companies can potentially train some kind of AI&amp;nbsp;models on the data (speaking purely about technical possibility, not legality of course; there aren&amp;rsquo;t any secret clauses in the ToS that let Bluesky sell your data to AI&amp;nbsp;companies)&lt;/li&gt;
&lt;li&gt;blocking someone only adds friction, but it can’t completely prevent them from seeing your posts (same as it always was on Twitter, since you could always open a post in an “incognito” window or use an alt account)&lt;/li&gt;
&lt;li&gt;there is no way to add a feature that would let you “lock” a profile for followers only, hiding your content from others, because all post data has to be public for the network to work&lt;/li&gt;
&lt;li&gt;copies of any posts you delete might still remain on some third party servers (most services do delete their copies when you delete a post on Bluesky, but you can&amp;rsquo;t prove it or enforce it)&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;Some of this might sound scary, but most of this is or was always the case on other social networks too, those that have APIs at least &amp;ndash; if you’re posting something publicly anywhere, you need to realize that anyone can record it forever. The main difference is that it’s &lt;em&gt;easier&lt;/em&gt; to do it here, because the API&amp;nbsp;has fewer restrictions than on centralized platforms, and that it’s currently not possible to do any semi-private content that’s visible to some people but not to others.&lt;/p&gt;

&lt;hr /&gt;

&lt;p id="security"&gt;&lt;/p&gt;

&lt;h2&gt;Account security&lt;/h2&gt;

&lt;p&gt;For logging in to third party apps and tools, Bluesky has initially added a temporary system of &amp;ldquo;app passwords&amp;rdquo;. Recently, they&amp;rsquo;ve also implemented more convenient OAuth authorization, where you authorize a service to use your account like on Twitter and other sites. However, it&amp;rsquo;s pretty complex to implement, so not all third party apps and tools support it yet, and you might still need to use the app passwords for some of those.&lt;/p&gt;

&lt;p&gt;An app password is a special one-time password that you can generate in the official app (Settings / App Passwords), which look something like this: &lt;code&gt;abcd-ef56-vxyz-qq34&lt;/code&gt;. You can generate a separate password for every tool and app that you log into. Such password grants &lt;em&gt;almost&lt;/em&gt; the same privileges as the main password, but without a few critical ones, and you can revoke it at any time from the Settings screen if you&amp;rsquo;re not using that app anymore, which disables access to your account for that app. You can also specify if the app password should give the app access to your DMs or not.&lt;/p&gt;

&lt;p&gt;For apps and tools that use OAuth, it works just like when authorizing apps to use your Twitter or Facebook account – you get redirected to your Bluesky server, you log in to your account, grant access for the app, and are redirected back there. You can see and manage your list of currently authorized apps on the &lt;a href="https://bsky.social/account/"&gt;account page&lt;/a&gt; (if your account is &lt;em&gt;not&lt;/em&gt; hosted on a Bluesky server, you need to open that on your server&amp;rsquo;s domain instead).&lt;/p&gt;

&lt;p&gt;For additional security, there is for now a simple &lt;a href="https://bsky.app/profile/bsky.app/post/3kqxv5yeof32a"&gt;email-based Two-Factor Authentication (2FA)&lt;/a&gt; feature (note: not currently working if you&amp;rsquo;re on a third-party server). A more complete 2FA system is coming.&lt;/p&gt;

&lt;hr /&gt;

&lt;p id="handles"&gt;&lt;/p&gt;

&lt;h2&gt;Handles &amp;amp; IDs&lt;/h2&gt;

&lt;p&gt;Bluesky has a really cool system of handles. They couldn’t have just used single-element handles like “&lt;a href="https://twitter.com/donaldtusk"&gt;@donaldtusk&lt;/a&gt;”, because that wouldn’t really make sense in a decentralized system. They also didn’t want to bind your account permanently to the name of the server you’re on like in Mastodon, where you’re e.g. “&lt;a href="https://tapbots.social/@ivory"&gt;ivory@tapbots.social&lt;/a&gt;” and you can’t change that unless you make a new account.&lt;/p&gt;

&lt;p&gt;So here’s what they’ve come up with: internally, your account is identified by a unique identifier called “DID” (Decentralized Identifier) – an ugly string of random letters (mine is &lt;code&gt;did:plc:oio4hkxaop4ao4wz2pp3f4cr&lt;/code&gt;). To that DID you assign a handle, which you can switch at any moment to a different one, and any contacts, references and connections will (mostly) stay intact; and the handle is actually just any domain name, usually displayed with an “@“ at the beginning, but without any additional username before it.&lt;/p&gt;

&lt;p&gt;By default, when you first join Bluesky (the current official server) you’re given a handle which is a subdomain of bsky.social, e.g. &amp;ldquo;&lt;a href="https://bsky.app/profile/georgetakei.bsky.social"&gt;georgetakei.bsky.social&lt;/a&gt;&amp;rdquo;. This is a real domain name, you can type it into the address bar of the browser and it will redirect you to the profile on Bluesky.&lt;/p&gt;

&lt;p&gt;But at any moment you can switch to a different handle by assigning any domain name that you own. It can a very short name like &lt;a href="https://bsky.app/profile/retr0.id"&gt;retr0.id&lt;/a&gt;, or something long with one of those quirky new TLDs like &lt;code&gt;.horse&lt;/code&gt; or &lt;code&gt;.computer&lt;/code&gt;, or it can even be a very official sounding domain like &lt;a href="https://bsky.app/profile/washingtonpost.com"&gt;washingtonpost.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There are two ways to assign a domain, either via HTTP by putting a file in a specific place on the website hosted on the domain, or via DNS by putting a new entry in your domain configuration &amp;ndash; &lt;a href="https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial"&gt;the complete instructions are here&lt;/a&gt;. (BTW, Bluesky also runs a service that resells and automatically configures domains through a &lt;a href="https://bsky.social/about/blog/7-05-2023-namecheap"&gt;partnership with Namecheap&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;This also means that you don’t need to rush to “reserve your handle” at ***.bsky.social &amp;ndash; because custom handles are cooler anyway 😎 (if you change your handle from *.bsky.social to a custom domain, that old handle is now &lt;a href="https://bsky.app/profile/did:plc:vjug55kidv6sye7ykr5faxxn/post/3ldjbkebpgs2g"&gt;reserved and unavailable for others&lt;/a&gt;, that didn&amp;rsquo;t work like this at first). This system also makes it much easier to move your account to a different server &amp;ndash; you can take your content and connections with you and keep your existing identity, because your DID identifier never changes.&lt;/p&gt;

&lt;p&gt;There&amp;rsquo;s even a number of &amp;ldquo;handle services&amp;rdquo; now that let anyone use a subdomain of their domain as the handle – e.g. go to &lt;a href="https://swifties.social"&gt;swifties.social&lt;/a&gt; if you want your handle to be &amp;ldquo;&lt;em&gt;yourname.swifties.social&lt;/em&gt;&amp;rdquo; 👩🏻‍🎤&lt;/p&gt;

&lt;p id="verification"&gt;&lt;/p&gt;

&lt;h3&gt;Verification&lt;/h3&gt;

&lt;p&gt;The system of domain names in handles serves as a primary way of verifying accounts on Bluesky. If someone has a handle that matches the domain of e.g. some newspaper, organization or government branch that you recognize, you can assume that the account is operated by someone who was authorized by that entity. So e.g. &lt;a href="https://bsky.app/profile/wyden.senate.gov"&gt;@wyden.senate.gov&lt;/a&gt; is definitely US Senator Wyden (or at least his staff), and &lt;a href="https://bsky.app/profile/cnn.com"&gt;@cnn.com&lt;/a&gt; is definitely CNN – no one has to manually verify their documents. We also have here e.g. the European Commision &lt;a href="https://bsky.app/profile/ec.europa.eu"&gt;@ec.europa.eu&lt;/a&gt;, government of Brazil &lt;a href="https://bsky.app/profile/brasil.gov.br"&gt;@brasil.gov.br&lt;/a&gt;, Interpol &lt;a href="https://bsky.app/profile/interpol.int"&gt;@interpol.int&lt;/a&gt;, or European Space Agency &lt;a href="https://bsky.app/profile/esa.int"&gt;@esa.int&lt;/a&gt; 🚀&lt;/p&gt;

&lt;p&gt;If you&amp;rsquo;re setting up an account for some well known organization, it&amp;rsquo;s &lt;strong&gt;highly recommended&lt;/strong&gt; to switch to your domain as the handle from the start to make it clear that it&amp;rsquo;s an official account.&lt;/p&gt;

&lt;p&gt;As an additional verification factor, e.g. for publicly known people who don&amp;rsquo;t have a well known domain or can&amp;rsquo;t use one, Bluesky has now added classic &lt;a href="https://bsky.social/about/blog/04-21-2025-verification"&gt;&amp;ldquo;blue check&amp;rdquo; verification badges&lt;/a&gt;. They&amp;rsquo;re not really meant as a common thing like they are on Twitter now, and they have some important limitations, e.g. they expire if you modify your handle or display name in any way, but they&amp;rsquo;re useful for making sure that, for example, yes, it&amp;rsquo;s the real &lt;a href="https://bsky.app/profile/did:plc:5c6cw3veuqruljoy5ahzerfx"&gt;Barack Obama&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The checkmark verification system is designed in such a way that Bluesky can grant other organizations the right to verify the people they vouch for themselves; so you can see that e.g. a &lt;a href="https://bsky.app/profile/did:plc:mymiyz3x72kdfeb5nfmrk5my"&gt;Financial Times journalist&lt;/a&gt; has a blue checkmark, but if you click on it, you see that he&amp;rsquo;s been verified by Financial Times, not by the Bluesky team. That way, Bluesky folks don&amp;rsquo;t need to verify each account themselves manually.&lt;/p&gt;

&lt;hr /&gt;

&lt;p id="terms"&gt;&lt;/p&gt;

&lt;h2&gt;How are things called here?&lt;/h2&gt;

&lt;p&gt;The app generally uses “neutral” terms like “post”, “repost”, “feed”, “timeline” and so on. That said, pretty early on someone came up with with the word “skeet” for posts, for “sky + tweet”, and it stuck, despite (or likely because of) the &lt;a href="https://bsky.app/profile/did:plc:oky5czdrnfjpqslsw2a5iclo/post/3juflvnb3d62u"&gt;team’s protests&lt;/a&gt;. So the term is used pretty commonly, though maybe a bit ironically, despite being a bit controversial because of its existing, other slang meaning (see Urban Dictionary)… By analogy, you can also “reskeet”, “subskeet” and so on.&lt;/p&gt;

&lt;p&gt;The timeline/home feed is also sometimes called a “skyline”.&lt;/p&gt;

&lt;p&gt;New users that have joined Bluesky recently are often called “newskies” &amp;ndash; there is even a &lt;a href="https://bsky.app/profile/did:plc:wzsilnxf24ehtmmc3gssy5bu/feed/newskies"&gt;Newskies feed&lt;/a&gt;, which includes every new user’s first post (and only their first post). On the opposite end, some folks sometimes jokingly call people with a long experience on the platform “Bluesky elders” (a reference to one post that was widely made fun of). There is &lt;a href="https://bsky.app/profile/did:plc:e4elbtctnfqocyfcml6h2lf7"&gt;a labeller&lt;/a&gt; that gives you an elder label if you had an account before summer 2023.&lt;/p&gt;

&lt;p&gt;A “&lt;a href="https://knowyourmeme.com/memes/events/hellthread-hellrope-bluesky"&gt;hellthread&lt;/a&gt;” is something that existed for some time in the spring of last year &amp;ndash; the initial implementation of notifications notified you of any reply somewhere below your post or comment, to an unlimited depth. People have started creating extremely long and nested threads, which notified everyone involved of any reply, and there was no way to opt out of it. A certain community has formed around these hellthreads, of people who hang out together and sometimes waged wars in them. Eventually, the notifications were fixed, but due to protests, the devs have kept an option to still create hellthreads in one place, hardcoded in the app code. This was finally removed a few months later.&lt;/p&gt;

&lt;p&gt;A &amp;ldquo;nuclear block&amp;rdquo;, alternately &amp;ldquo;apocablock&amp;rdquo;, is a name for the feature where when one user blocks another, it hides all previous replies between the two from everyone – it basically &amp;ldquo;nukes&amp;rdquo; the whole conversation to discourage other people from joining in (though you can still find the posts though if you&amp;rsquo;re determined, e.g. on the users' profile page feeds or in &lt;a href="https://blue.mackuba.eu/skythread/"&gt;Skythread&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A &amp;ldquo;contraption&amp;rdquo; was an informal name for set of popular mutelists/blocklists maintained by a user named Kairi in 2023 (which were later turned into a widely used labeller called Aegis, which eventually shut down a few months later) – the trolls and haters who have crossed a line were told &amp;ldquo;okay, you&amp;rsquo;re going into the contraption&amp;rdquo;. The term is still being used as a generic name for any set of blocklists.&lt;/p&gt;

&lt;hr /&gt;

&lt;p id="federation"&gt;&lt;/p&gt;

&lt;h2&gt;What is this federation thing?&lt;/h2&gt;

&lt;p&gt;The goal of Bluesky is to be a network that consists of many, many servers run by a lot of different companies, organizations and people that connect with each other &amp;ndash; that’s roughly what federation means. Initially, all the critical pieces were controlled by Bluesky PBC; however, this is gradually changing. They&amp;rsquo;ve taken the first big step towards federation in February 2024, by &lt;a href="https://bsky.social/about/blog/02-22-2024-open-social-web"&gt;letting people migrate their accounts to self-hosted servers&lt;/a&gt;. This is still a technically complex process, and there aren&amp;rsquo;t really any public non-Bluesky servers which allow open signup, but there are currently a &lt;a href="https://blue.mackuba.eu/directory/pdses"&gt;couple thousands&lt;/a&gt; of mostly personal &amp;ldquo;PDS&amp;rdquo; servers, where people keep their accounts and data. I&amp;rsquo;m currently posting from my own PDS named &lt;a href="https://internect.info/did/did:plc:oio4hkxaop4ao4wz2pp3f4cr"&gt;lab.martianbase.net&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you’re worried that things will get more complicated, maybe you have some bad experiences from Mastodon &amp;ndash; then don’t worry. They’ve specifically designed everything to be less confusing. The Bluesky-managed accounts have actually already been spread out internally on a number of separate servers &amp;ndash; they’ve been migrated sometime in November 2023, and few people have noticed, because everything just kept working. Now, you have an option to move your account to a server controlled by someone else, if you want to &amp;ndash; but you can just ignore the whole thing if you don’t care about it. You can&amp;rsquo;t even easily see who is on which server, unless you check it with a third party tool like &lt;a href="https://internect.info"&gt;internect.info&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;(Fun fact: the Bluesky PDS servers are all named after &lt;a href="https://bsky-debug.app"&gt;different kinds of mushrooms&lt;/a&gt;, e.g. my original PDS was &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Amanita"&gt;amanita&lt;/a&gt;&amp;rdquo; 🍄 – so if you see someone talking about &amp;ldquo;mushroom servers&amp;rdquo;, they&amp;rsquo;re probably talking about those&amp;nbsp;:)&lt;/p&gt;

&lt;p id="bridgy"&gt;&lt;/p&gt;

&lt;h3&gt;Bridgy Fed&lt;/h3&gt;

&lt;p&gt;Bluesky opening to federation does not mean that it will connect with Mastodon servers though, since they use different, incompatible protocols. There is however a &lt;a href="https://snarfed.org/2024-05-04_52915"&gt;&amp;ldquo;bridge&amp;rdquo; service called Bridgy&lt;/a&gt;, built and operated by a third party developer, but supported by the Bluesky team.&lt;/p&gt;

&lt;p&gt;The way it works is that it &amp;ldquo;mirrors&amp;rdquo; Mastodon accounts and their posts to Bluesky or Bluesky accounts to Mastodon (for accounts that have enabled the bridge). It&amp;rsquo;s possible to create whole threads where some replies are made by Mastodon accounts and some by Bluesky accounts, all of them being visible in both places. See e.g. here the bridged profile of &lt;a href="https://bsky.app/profile/Gargron.mastodon.social.ap.brid.gy"&gt;Eugen Roshko (Gargron)&lt;/a&gt;, creator of Mastodon, as seen on Bluesky.&lt;/p&gt;

&lt;p&gt;If you want your Bluesky profile to be visible on the Fediverse, you need to &amp;ldquo;opt in&amp;rdquo; by following the official Bridgy account on Bluesky, &lt;a href="https://bsky.app/profile/ap.brid.gy"&gt;@ap.brid.gy&lt;/a&gt;. Your profile will be seen on the Fedi side as &lt;code&gt;@your.handle@atproto.brid.gy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To follow Mastodon accounts that are already bridged (like Gargron&amp;rsquo;s) from the Bluesky side, just look them up in the search and follow them as normally. If someone you want to follow is not bridged yet, you need to either ask them yourself to enable Bridgy (by following the Mastodon equivalent of the Bridgy account, &lt;code&gt;@atproto.brid.gy@atproto.brid.gy&lt;/code&gt;), or you can DM the &lt;a href="https://bsky.app/profile/ap.brid.gy"&gt;@ap.brid.gy&lt;/a&gt; account on Bluesky and tell the bot the Mastodon handle of the user you want to follow, and it will ask them on your behalf.&lt;/p&gt;

&lt;p&gt;Note that there are some obvious limitations because of the differences in supported features, e.g.:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;posts from Mastodon longer than 300 characters are truncated, and a link is added to the full original post&lt;/li&gt;
&lt;li&gt;Bluesky doesn&amp;rsquo;t support post editing yet, so edits made after you create a post aren&amp;rsquo;t visible on Bluesky&lt;/li&gt;
&lt;/ul&gt;


&lt;p id="atmosphere"&gt;&lt;/p&gt;

&lt;h3&gt;The Atmosphere&lt;/h3&gt;

&lt;p&gt;The &amp;ldquo;Atmosphere&amp;rdquo;, also often spelled &amp;ldquo;ATmosphere&amp;rdquo;, is a name given to the wider network of apps and services that are being built by third party developers on the Bluesky platform and the AT Protocol. It includes not only Bluesky-specific clients and tools, but also somewhat separate apps/sites like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;blogging platforms, e.g. &lt;a href="https://whtwnd.com"&gt;WhiteWind&lt;/a&gt; or &lt;a href="https://about.leaflet.pub"&gt;Leaflet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;photo sharing sites like &lt;a href="https://grain.social"&gt;Grain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tangled.org"&gt;Tangled&lt;/a&gt;, a GitHub-like code hosting &amp;amp; collaboration site&lt;/li&gt;
&lt;li&gt;the &lt;a href="#apps"&gt;video apps&lt;/a&gt; mentioned earlier&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;These are not directly a part of Bluesky, aren&amp;rsquo;t run by Bluesky PBC and aren&amp;rsquo;t always closely integrated with Bluesky,  but they are built on the same system, and depending on the specific tool, they can potentially integrate and share data with Bluesky and with each other. In each of those, you can usually sign in using your existing Bluesky account and use the same handle/avatar/name that you have configured on Bluesky.&lt;/p&gt;

&lt;hr /&gt;

&lt;p id="search"&gt;&lt;/p&gt;

&lt;h2&gt;Search&lt;/h2&gt;

&lt;p&gt;Search works pretty well now on Bluesky. You can search for words, phrases and hashtags, and sort by popular posts or by latest. This is a global search like on Twitter, so it finds posts from everyone everywhere, not like the limited full text search on Mastodon.&lt;/p&gt;

&lt;p&gt;You can also add “from:some.handle” to find only posts from a given user, or “from:me” to search within your own posts. There are several more filters available, like filtering by date or by language, although there&amp;rsquo;s no easy UI&amp;nbsp;for everything yet – but the Bluesky team has posted a tutorial &amp;ldquo;&lt;a href="https://bsky.social/about/blog/05-31-2024-search"&gt;Tips and Tricks for Bluesky Search&lt;/a&gt;&amp;rdquo; on their blog recently.&lt;/p&gt;

&lt;p id="hashtags"&gt;&lt;/p&gt;

&lt;h2&gt;Hashtags&lt;/h2&gt;

&lt;p&gt;Support for hashtags was added &lt;a href="https://bsky.app/profile/bsky.app/post/3kmjbfqrrbu2t"&gt;in February 2024&lt;/a&gt;. When you click on a hashtag you have an option to search for all posts with this hashtag, only posts from the given person, or to mute that hashtag.&lt;/p&gt;

&lt;p&gt;Note that for various reasons, hashtags aren&amp;rsquo;t as integral part of the platform and aren&amp;rsquo;t as commonly used here as on the Fediverse; people also are wary of using them because they have a bit of a bad reputation from Twitter or Instagram, where spammers often abuse them. Try not to overdo hashtags, e.g. don&amp;rsquo;t use more than 1-2 in a post – ideally just take note of how other people are using them.&lt;/p&gt;

&lt;p&gt;One thing that&amp;rsquo;s defined in the protocol but not implemented in the official app yet is that hashtags were designed to have two forms: inline tags like on Twitter, and external (outline) tags which are shown below the post, which I&amp;nbsp;think is the way they work on Tumblr (Mastodon now also shows trailing hashtags at the end of a post in a similar way). You will be able to use either or both in a post according to your preference, and they will be interchangeable, with both being returned in the same search. These tags are currently supported in some third party apps like Skeetdeck.&lt;/p&gt;

&lt;p id="video"&gt;&lt;/p&gt;

&lt;h2&gt;Video &amp;amp; GIFs&lt;/h2&gt;

&lt;p&gt;Videos are available on Bluesky since &lt;a href="https://bsky.social/about/blog/09-11-2024-video"&gt;September 2024&lt;/a&gt;. They&amp;rsquo;re currently limited to 3 minutes.&lt;/p&gt;

&lt;p&gt;For GIFs, there is a built-in support for adding Tenor GIFs in the post compose dialog, by pressing the GIF button and picking something from the search results, and you can also upload custom GIFs – now also on mobile.&lt;/p&gt;

&lt;p&gt;You can also embed e.g. YouTube videos in posts, which will play inline inside a post when clicked. (This whole feature was actually &lt;a href="https://github.com/bluesky-social/social-app/pull/2217"&gt;implemented by a third party developer&lt;/a&gt;.)&lt;/p&gt;

&lt;p id="starter-packs"&gt;&lt;/p&gt;

&lt;h2&gt;Starter packs&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://bsky.social/about/blog/06-26-2024-starter-packs"&gt;Starter packs&lt;/a&gt; are a unique Bluesky feature added in June &amp;lsquo;24, which lets you create a list of accounts and feeds that you want to recommend to others. You can make e.g. a list of &lt;a href="https://bsky.app/starter-pack/did:plc:oio4hkxaop4ao4wz2pp3f4cr/3kvucqy663j2l"&gt;AT Protocol developers&lt;/a&gt;, &lt;a href="https://bsky.app/starter-pack/did:plc:77lswp42lgjyw36ozuo7kt7e/3l22w7vfug32w"&gt;climate scientists and reporters&lt;/a&gt;, or &lt;a href="https://bsky.app/starter-pack/did:plc:jcoy7v3a2t4rcfdh6i4kza25/3kvvsi4qacz2p"&gt;astronomers&lt;/a&gt;, and then people can use it to find some interesting new accounts to follow, or just follow them all at once with one click.&lt;/p&gt;

&lt;p&gt;You can also share a link to a starter pack with people who don&amp;rsquo;t have a Bluesky account yet, and they can use it to sign up with to have some initial set of accounts to follow.&lt;/p&gt;

&lt;p&gt;There&amp;rsquo;s no list/search of starter packs built-in in the app, but you can browse and search for them e.g. on the third party &lt;a href="https://blueskydirectory.com"&gt;Bluesky Directory&lt;/a&gt; website.&lt;/p&gt;

&lt;p id="trends"&gt;&lt;/p&gt;

&lt;h2&gt;Trending topics&lt;/h2&gt;

&lt;p&gt;There is now a simple &amp;ldquo;trending&amp;rdquo; section that shows up in the search section and in the right sidebar on the web. Every entry leads to a kind of custom feed about that topic. You can easily disable this section if you&amp;rsquo;d prefer to not be distracted by current events, and if you&amp;rsquo;d like to bring it back, you can turn it on or off in the settings (&amp;ldquo;Content &amp;amp; Media&amp;rdquo; section).&lt;/p&gt;

&lt;p id="bookmarks"&gt;&lt;/p&gt;

&lt;h2&gt;Bookmarks&lt;/h2&gt;

&lt;p&gt;Native bookmarks (called &amp;ldquo;saved posts&amp;rdquo;) were added in &lt;a href="https://bsky.app/profile/bsky.app/post/3lydt7uwac22f"&gt;early September&lt;/a&gt;. You can save a post using the &amp;ldquo;bookmark&amp;rdquo; icon below it, and you can access saved posts through the &amp;ldquo;Saved&amp;rdquo; section in the sidebar. Bookmarks are private – a counter of number of saves is shown below a post, but you can&amp;rsquo;t see who saved it.&lt;/p&gt;

&lt;p&gt;An earlier, popular workaround was that you could reply to posts with a comment &amp;ldquo;📌&amp;rdquo;, and there&amp;rsquo;s a special &lt;a href="https://bsky.app/profile/did:plc:q6gjnaw2blty4crticxkmujt/feed/my-pins"&gt;bookmark feed&lt;/a&gt; which shows you all your comments with that emoji. This option is still available, although since these bookmarks are essentially normal comments, they are all public.&lt;/p&gt;

&lt;p&gt;There is also &lt;a href="https://pin2saved.vercel.app"&gt;a tool available&lt;/a&gt; to import all your previous &amp;ldquo;pins&amp;rdquo; to the new &amp;ldquo;saved posts&amp;rdquo; system.&lt;/p&gt;

&lt;hr /&gt;

&lt;p id="settings"&gt;&lt;/p&gt;

&lt;h2&gt;Settings&lt;/h2&gt;

&lt;p&gt;Some things that you may want to look at in the settings, going from the top:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;u&gt;Switch account&lt;/u&gt; &amp;ndash; you can log in to multiple accounts, and you can switch between them here in the Settings, by clicking your avatar in the top-left corner of the page on the web, or by pressing and holding your avatar in the tab bar on mobile&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Account &amp;raquo; Export my data&lt;/u&gt; &amp;ndash; lets you download a backup of your data like posts and follows (at the moment it doesn&amp;rsquo;t include media like images)&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Account &amp;raquo; Deactivate account&lt;/u&gt; &amp;ndash; lets you temporarily lock your account (it appears deleted to others)&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Privacy and Security&lt;/u&gt; &amp;ndash; enable Two-Factor Authentication and create &amp;ldquo;app passwords&amp;rdquo; for third party apps; you can also specify if you want others to be able to get notifications whenever you make a new post&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Moderation&lt;/u&gt; &amp;ndash; manage muted words &amp;amp; tags, muted and blocked accounts, and your moderation lists; you can also choose to hide &amp;ldquo;blue check&amp;rdquo; badges if they annoy you&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Moderation &amp;raquo; Content Filters&lt;/u&gt; &amp;ndash; here you can set what kind of potentially objectionable content you may want to show or hide &amp;ndash; generally various NSFW things. I’m not sure what the defaults are currently, but it’s worth checking and tweaking to your preferences. (Watch out, there can be quite a lot of somewhat NSFW content there that you can randomly come across in some feeds, although it’s generally hidden behind a content warning.)
&lt;div&gt;In the &amp;ldquo;Advanced&amp;rdquo; section below, you can adjust which of the moderation labels from each specific labeller you want to apply in your feeds – this includes the built-in &amp;ldquo;Bluesky Moderation Service&amp;rdquo; labeller.&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Notifications&lt;/u&gt; &amp;ndash; since July 2025, you can configure here precisely what kind of notifications you want to get: you can e.g. turn off all notifications about likes, reposts or follows, or leave them on only for the people you follow&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Content &amp;amp; Media &amp;raquo; Thread Preferences&lt;/u&gt; &amp;ndash; you can choose to have threads sorted by newest, oldest or most liked comments; there is also an experimental nested (tree-like) thread view that I&amp;nbsp;highly recommend enabling (although for longer threads you may want to use my tool &lt;a href="https://blue.mackuba.eu/skythread/"&gt;Skythread&lt;/a&gt; instead&amp;nbsp;:]&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Content &amp;amp; Media &amp;raquo; Following Feed Preferences&lt;/u&gt; &amp;ndash; choose if you want to see replies, quotes, reposts etc. in the home feed. (Some earlier options from this section were removed last year, see the &lt;a href="#feeds"&gt;Feeds&lt;/a&gt; section for more info &amp;amp; workaround.)&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Content &amp;amp; Media &amp;raquo; Autoplay videos and GIFs&lt;/u&gt; &amp;ndash; turn off to prevent videos and gifs from automatically playing in your feeds; instead you get a play button on each that you have to press first to see it&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Content &amp;amp; Media &amp;raquo; Enable trending topics&lt;/u&gt; &amp;ndash; turn off if doomscrolling isn&amp;rsquo;t your thing&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Appearance&lt;/u&gt; &amp;ndash; you can make the fonts smaller or larger, or switch light/dark mode&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Accessibility &amp;raquo; Require alt text before posting&lt;/u&gt; &amp;ndash; you can turn this on to always be reminded to set an &lt;a href="https://accessibility.huit.harvard.edu/describe-content-images"&gt;alt text on photos&lt;/a&gt; (it&amp;rsquo;s generally considered nice to add the alt text to images whenever possible, for people who use tools like screen readers or VoiceOver; as a bonus, it also makes your posts easier to find in search and feeds)&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Languages&lt;/u&gt; &amp;ndash; turn on all the languages that you understand or want to see; Bluesky generally hides posts in languages that you don’t have enabled from most places like feeds and search results, except your Following timeline. For your own posts, you set a post&amp;rsquo;s language yourself in the compose post window &amp;ndash; you can switch between a few languages, and if you have the wrong one set when writing, a popup with a warning should appear.&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;Also in the &amp;ldquo;Chat&amp;rdquo; section of the app, there is a separate &lt;u&gt;Chat Settings&lt;/u&gt; section under the &amp;ldquo;cog wheel&amp;rdquo; icon, where you can specify who you want to be able to contact you via DMs: everyone, no one, or only people that you follow yourself (default is people you follow); conversations you&amp;rsquo;ve started before are accessible even if you change the setting.&lt;/p&gt;

&lt;hr /&gt;

&lt;p id="missing"&gt;&lt;/p&gt;

&lt;h2&gt;Missing features&lt;/h2&gt;

&lt;p&gt;There are a few things missing on Bluesky that are available on some other networks. Some of these are limitations of the protocol, and some are just a matter of too much work and too few hands to do it &amp;ndash; the team is still pretty small and they have a ton of things to build to catch up with more mature platforms (Mastodon) or those with more people and funds (Threads), and they need to prioritize and some things are always put off.&lt;/p&gt;

&lt;p&gt;If you&amp;rsquo;re a developer and want to help, the app and most of the related services are &lt;a href="http://github.com/bluesky-social/social-app/"&gt;open source&lt;/a&gt;, and the team is happy to accept outside contributions at least for bug fixes and &lt;a href="https://github.com/bluesky-social/social-app/pull/2504"&gt;smaller tweaks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Some things that are still missing:&lt;/p&gt;

&lt;h4&gt;Private profiles / circles&lt;/h4&gt;

&lt;p&gt;Like I’ve mentioned in the &lt;a href="#privacy"&gt;Privacy section&lt;/a&gt;, the AT Protocol as built currently requires all data apart from your private settings to be completely public. There is no way to make something that you only share with &lt;em&gt;some&lt;/em&gt; people but not others. There may very well be something like this in the future, because &lt;em&gt;a lot&lt;/em&gt; of people are asking for this and the team wants to look into it &amp;ndash; but they will have to first invent some other, separate way to share content in the protocol, which will take a lot of time and thinking. So it’s likely to come at some point, but not anytime soon, because it’s much harder than it may look.&lt;/p&gt;

&lt;h4&gt;DMs on the protocol&lt;/h4&gt;

&lt;p&gt;For the same reason, sharing messages with only one or a few people using the AT Protocol is currently not possible. The team wants to eventually add DMs to the protocol, but this is something that will require a lot of research first.&lt;/p&gt;

&lt;p&gt;But since *a lot* of people wanted to have &lt;em&gt;some&lt;/em&gt; way of talking privately with friends, even if it&amp;rsquo;s an imperfect one, the team has recently added a simple implementation of DMs that isn&amp;rsquo;t currently a part of the protocol. The DMs are currently using a single centralized service hosted by Bluesky (although third-party apps can access this API), and are not end-to-end encrypted – so they basically work like on Twitter. The first version also doesn&amp;rsquo;t support group chats and images, only 1-to-1 text chat – but more features are coming soon.&lt;/p&gt;

&lt;p&gt;Eventually, the team wants to figure out and add a more full-featured, decentralized and private version of DMs (which may involve integrating with some existing private messages standard). This is however pretty far down the list at the moment, so something not likely to come this year.&lt;/p&gt;

&lt;h4&gt;Post editing&lt;/h4&gt;

&lt;p&gt;This will definitely be added at some point, but it’s also a relatively complex feature, because of the need to store the previous versions of an edited post that you should be able to access somehow. They want to do it right and that will require some thinking on how to solve the problem in the most general and elegant way.&lt;/p&gt;

&lt;h4&gt;Soft-blocking / removing followers&lt;/h4&gt;

&lt;p&gt;There is an issue currently that there is no way to remove someone from your followers except by having them blocked. If you block-and-unblock them, what some people call “soft blocking”, they stay on the followers list. This is a current limitation of how follows are designed in the protocol &amp;ndash; when someone follows you, they do it by adding a “follow record” to their account, and only they can update or delete their own records, you can’t do that from your side.&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;think this is likely to get fixed at some point with some kind of workaround, but it’s not trivial to add and not high priority at the moment – but they&amp;rsquo;re thinking about it.&lt;/p&gt;

&lt;h4&gt;Polls&lt;/h4&gt;

&lt;p&gt;This might be a bit tricky, because we probably don’t want to have everyone’s poll choices public to everyone, and right now everything is public… But this is also one of those things that people ask about regularly, so I&amp;nbsp;hope they’ll figure something out.&lt;/p&gt;

&lt;h4&gt;Two-factor authentication&lt;/h4&gt;

&lt;p&gt;See the &amp;ldquo;&lt;a href="#security"&gt;Account security&lt;/a&gt;&amp;rdquo; section.&lt;/p&gt;

&lt;h4&gt;Longer posts&lt;/h4&gt;

&lt;p&gt;This seems to have been a conscious decision that the team wanted to create a medium that’s more like Twitter than like Mastodon when it comes to post length. This isn’t a simple matter of a field length, because this affects the way people communicate, and allowing longer posts has advantages and disadvantages, it’s just a different text form. (&lt;a href="https://news.ycombinator.com/item?id=39551004"&gt;See a comment from lead dev about this.&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;So it looks like this won’t be changing &amp;ndash; although there might later be other “apps” implemented on the AT Protocol that aren’t a part of Bluesky that will allow longer posts, even complete articles, and they might be somehow integrated into Bluesky in the future (e.g. posts bridged from Mastodon by Bridgy, which can be up to 500 characters long, include the full original content in the record data and apps may choose to display the full text inline).&lt;/p&gt;

&lt;h4&gt;Disabling reposts per user&lt;/h4&gt;

&lt;p&gt;It&amp;rsquo;s not a hard technical problem, but it seems to be blocked by some other things right now – but it should be added eventually, since everyone is asking about it.&lt;/p&gt;

&lt;h4&gt;Links on the profile, scheduling, &amp;hellip;&lt;/h4&gt;

&lt;p&gt;I&amp;nbsp;haven’t heard much about those, but I’m assuming it’s all coming at some point once they get through the hard stuff they&amp;rsquo;re busy with now. (Scheduling can currently be done using some third party clients like &lt;a href="https://deck.blue"&gt;deck.blue&lt;/a&gt;, or cross-posting services like &lt;a href="https://buffer.com"&gt;Buffer&lt;/a&gt;.)&lt;/p&gt;

&lt;hr /&gt;

&lt;p id="tools"&gt;&lt;/p&gt;

&lt;h2&gt;Other tools&lt;/h2&gt;

&lt;p&gt;Finally, here are a few tools written by third party devs that you might find useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://firesky.tv"&gt;Firesky&lt;/a&gt; &amp;ndash; a site that shows you a live feed of every single new post made on Bluesky &amp;ndash; feels like watching the Matrix screen&lt;/li&gt;
&lt;li&gt;&lt;a href="https://clearsky.app/"&gt;Clearsky&lt;/a&gt; &amp;ndash; lets you look up the list of all people that are blocking you (or someone else), or the mute/block lists, user lists, and starter packs that you’ve been added to&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:yatb2t26fw7u3c7qcacq7rje"&gt;Listifications&lt;/a&gt; &amp;nbsp; sends notifications when you&amp;rsquo;re added to a list or starter pack, or when someone blocks you&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wolfgang.raios.xyz"&gt;wolfgang.raios.xyz&lt;/a&gt; &amp;ndash; also shows your blockers, block statistics and can generate “interaction circle” images that show who you mostly keep in touch with&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cred.blue"&gt;cred.blue&lt;/a&gt; &amp;ndash; calculates your &amp;ldquo;Bluesky score&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.jazco.dev/cleanup"&gt;Jaz&amp;rsquo;s Profile Cleaner&lt;/a&gt; &amp;ndash; lets you delete old data (old posts etc.) from your account&lt;/li&gt;
&lt;li&gt;&lt;a href="https://internect.info"&gt;internect.info&lt;/a&gt; &amp;ndash; lets you look up the DID of an account, its server name and how the assigned handle has changed in the past&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.jazco.dev/stats"&gt;Jaz’s post stats&lt;/a&gt; (total count and daily posters), &lt;a href="https://blue.mackuba.eu/stats/"&gt;my stats&lt;/a&gt; (daily/weekly stats), and &lt;a href="https://bskycharts.edavis.dev/edavis.dev/bskycharts.edavis.dev/index.html"&gt;bskycharts&lt;/a&gt; (charts of firehose activity and daily/monthly active users)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blue.mackuba.eu/skythread/"&gt;Skythread&lt;/a&gt; &amp;ndash; a thread reader by yours truly, mentioned earlier&amp;nbsp;:]&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blue.mackuba.eu/scanner/"&gt;Label Scanner&lt;/a&gt;, for checking if there are any labels assigned to an account or post&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mary-ext.codeberg.page/bluesky-quiet-posters/"&gt;Bluesky quiet posters&lt;/a&gt; – shows the list of people you follow ordered by how long ago they&amp;rsquo;ve posted anything&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cleanfollow-bsky.pages.dev"&gt;Clean follow&lt;/a&gt; – lets you clean up your follows list of blocked and suspended accounts&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.cam.fyi/unfollow"&gt;Gentle Unfollow&lt;/a&gt; – lets you browse your follows one by one, showing each person&amp;rsquo;s recent posts, and lets you decide if you want to keep them or unfollow&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky-follow-finder.theo.io"&gt;Bluesky network analyzer&lt;/a&gt; – analyzes your 2nd degree connections (follows of your follows), and shows you suggestions of people that a lot of your friends are following&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nws-bot.us/bskyListCombiner.php"&gt;nws-bot.us tools&lt;/a&gt; – a set of tools for managing lists and starter packs&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;And some other guides (some mentioned earlier in the blog post):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.google.com/document/d/1HVx7prLajuTZ_naAC3WyqJu4loaf2FRxPZSHSUxKsgk/view"&gt;Bluesky Guide&lt;/a&gt; (Amanda Wyatt Visconti, Aug 2023 + updates)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.archive.org/web/20240620103427/https://from-over-the-horizon.ghost.io/bluesky-social-new-user-guide/"&gt;Bluesky Social New User Guide&lt;/a&gt; (Kairi, Nov 2023 – archived)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://emilydoesastro.com/posts/230824-bluesky-signup/"&gt;How to get started on Bluesky&lt;/a&gt; (Emily Hunt, Nov 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/5-19-2023-user-faq"&gt;Bluesky user FAQ&lt;/a&gt; (Bluesky, May 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://write.as/y5kzn9moj6ohs30l.md"&gt;The Newskies' Guide to Safety and Privacy on Bluesky&lt;/a&gt; (eepy, Oct 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial"&gt;How to set your domain as your handle&lt;/a&gt; (Bluesky, Apr 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/05-31-2024-search"&gt;Tips and Tricks for Bluesky Search&lt;/a&gt; (Bluesky, May 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.archive.org/web/20250321051549/https://goodfeeds.co/the-guide"&gt;The Guide to Feeds&lt;/a&gt; (Jerry Chen – archived)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.social/about/blog/7-27-2023-custom-feeds"&gt;Algorithmic Choice with Custom Feeds&lt;/a&gt; (Bluesky, Jul 2023)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atpota.to/guides/bluesky-for-brands"&gt;How to use Bluesky to grow your brand&lt;/a&gt; (Dame, Jun 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marvins-guide.leaflet.pub/3lyqxqbbqkc2p"&gt;What the hell is the atmosphere anyway: A slightly less technical intro to the technical side of Bluesky&lt;/a&gt; (Bailey Townsend, Sep 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://overreacted.io/open-social/"&gt;Open Social&lt;/a&gt; – a high-level overview of how the AT Protocol brings back the openness of the original pre Web 2.0 web (Dan Abramov, Sep 2025)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.archive.org/web/20240620103516/https://from-over-the-horizon.ghost.io/bluesky-crash-course-labelers/"&gt;Bluesky Crash Course: Labelers&lt;/a&gt; (Kairi, Apr 2024 – archived)&lt;/li&gt;
&lt;/ul&gt;


&lt;hr /&gt;

&lt;p&gt;Thanks to Mozzius, Shreyan and Marshal for the feedback on the first draft&amp;nbsp;:)&lt;/p&gt;

&lt;p id="changelog"&gt;&lt;/p&gt;

&lt;h3&gt;Changelog:&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;18 Nov 2025&lt;/strong&gt;: added links to Tokimeki, Catbird, Dhaaga, and Graze, removed link to Skychat&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;26 Sep 2025&lt;/strong&gt;: added links to Bailey Townsend&amp;rsquo;s and Dan Abramov&amp;rsquo;s blog posts&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8 Sep 2025&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;added native bookmarks&lt;/li&gt;
&lt;li&gt;custom GIF upload now works on mobile&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;&lt;strong&gt;29 Jul 2025&lt;/strong&gt;: added link to guide by Amanda Wyatt Visconti&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9 Jul 2025&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;added new sections about trending topics, verification badges, and the Atmosphere&lt;/li&gt;
&lt;li&gt;videos have a 3-minute limit now&lt;/li&gt;
&lt;li&gt;mentioned several new photo and video apps, and Tapbots' plans for their Bluesky client&lt;/li&gt;
&lt;li&gt;added links to a few new sites and tools, removed US Politics Labeller, Skybridge and PLC Handle Tracker&lt;/li&gt;
&lt;li&gt;reordered &amp;amp; expanded settings page descriptions&lt;/li&gt;
&lt;li&gt;mentioned Bluesky&amp;rsquo;s subscription plans&lt;/li&gt;
&lt;li&gt;mentioned recent fixes in Discover&lt;/li&gt;
&lt;li&gt;mentioned that subscriptions for account new post notifications are private&lt;/li&gt;
&lt;li&gt;mentioned reporting malicious lists&lt;/li&gt;
&lt;li&gt;mentioned Ozone and country-specific moderation&lt;/li&gt;
&lt;li&gt;added info that .bsky.social handles are now kept reserved after change&lt;/li&gt;
&lt;li&gt;added some tips about hashtags use&lt;/li&gt;
&lt;li&gt;added info about OAuth account management page&lt;/li&gt;
&lt;li&gt;added recommendation of Signal for sensitive DM conversations&lt;/li&gt;
&lt;li&gt;added info about GIF uploads&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;&lt;strong&gt;12 Nov 2024&lt;/strong&gt;: added info about threads composer and the plans for disabling reposts&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8 Nov 2024&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;added info or mentions of new features: videos, starter packs, pinned posts, listing quotes, timed word muting, new safety features, and OAuth&lt;/li&gt;
&lt;li&gt;added a list of popular labellers&lt;/li&gt;
&lt;li&gt;added a mention of handle services&lt;/li&gt;
&lt;li&gt;expanded the &amp;ldquo;how are things called&amp;rdquo; section a bit&lt;/li&gt;
&lt;li&gt;updated section about feeds, added a list of recommended feeds&lt;/li&gt;
&lt;li&gt;updated section about federation, added info about Bridgy Fed&lt;/li&gt;
&lt;li&gt;updated info about Jack Dorsey&amp;rsquo;s involvement&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;&lt;strong&gt;24 Jun 2024&lt;/strong&gt;: removed link to Aegis (rip)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;19 Jun 2024&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;added info about built-in Tenor GIFs and DMs&lt;/li&gt;
&lt;li&gt;added new section about labellers&lt;/li&gt;
&lt;li&gt;Jack Dorsey is no longer on the board&lt;/li&gt;
&lt;li&gt;updated some mentions about the Mastodon bridge, which is now live&lt;/li&gt;
&lt;li&gt;some changes to feeds section – defaults for Following have changed, Discover is now much better, and you can remove the default feeds&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;&lt;strong&gt;26 Mar 2024&lt;/strong&gt;: added mention about handle history in the Privacy section&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2 Mar 2024&lt;/strong&gt;: hashtags and word muting are now available, updated the part about longer posts&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;23 Feb 2024&lt;/strong&gt;: federation is live!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;21 Feb 2024&lt;/strong&gt;: search now returns results when you search for a hashtag.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://mackuba.eu/2023/11/09/year-of-social-media-coding/</id>
    <title>2023: Year of social media coding</title>
    <published>2023-11-09T14:46:07Z</published>
    <updated>2023-11-09T14:46:07Z</updated>
    <link href="https://mackuba.eu/2023/11/09/year-of-social-media-coding/"/>
    <content type="html">&lt;p&gt;I&amp;nbsp;had different plans for this year… then, Elon Musk happened.&lt;/p&gt;

&lt;p&gt;Elon took over Twitter in October last year, which set many different processes in motion. A lot of people I&amp;nbsp;liked and followed started leaving the platform. Mastodon and the broader Fediverse, which has been slowly growing for many years but never got anything close to being mainstream, suddenly blew up with activity. A lot of those people I&amp;nbsp;was following ended up there.&lt;/p&gt;

&lt;p&gt;Then, Twitter started getting progressively worse under the new management. Elon&amp;rsquo;s antics, the whole blue checks / verification clusterfuck, killing off third party apps and effectively shutting down the API, locking the site behind a login wall, finally renaming the app and changing the logo – each step made some of the users lose interest in the platform, making it gradually less interesting and harder to use.&lt;/p&gt;

&lt;p&gt;Changes, so many changes… and things changing meant that I&amp;nbsp;had to change my workflows, change some plans, build a whole bunch of new tools, change plans a few times again, and so on. My GitHub looks like this right now, which is way above the average of previous years:&lt;/p&gt;
&lt;p class="image"&gt;&lt;img alt="GitHub green squares activity chart, pretty green" src="https://mackuba.eu/images/posts/social2023/github.png?1771213129" width="640"&gt;&lt;/p&gt;

&lt;p&gt;As usual, I&amp;nbsp;ended up writing way more Ruby and JavaScript than Swift, which goes a bit against my general career plans – but I’ve built so much stuff this year and I&amp;nbsp;had a ton of fun doing it. So in this blog post, I&amp;nbsp;wanted to share some of the things I’ve been working on lately.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;The Dead Bird Site 🦤&lt;/h2&gt;

&lt;p&gt;I&amp;nbsp;had a bunch of private tools written for the Twitter API. For example, I&amp;nbsp;had a script that downloaded all tweets from my timeline and some lists to a local database. I&amp;nbsp;was also running various statistics on tweets, e.g. which people contribute how much to the timeline and list feeds, and automatically extracted links from tweets from some selected lists.&lt;/p&gt;

&lt;p&gt;And then Elon shut off access to the API&amp;nbsp;(unless you can afford $100 per month for a &amp;ldquo;hobbyist&amp;rdquo; plan), which meant I&amp;nbsp;had to try to find other ways to get that data.&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;quickly got the idea that I&amp;nbsp;could somehow intercept the JSON responses that the Twitter webapp (I&amp;nbsp;refuse to call it the new name, sue me) is loading from the JavaScript code. The JSON responses are very complicated with a lot of extra content, but they do contain everything I&amp;nbsp;need. The problem is how to get them; I&amp;nbsp;wanted to get data from my personal timelines, so I&amp;nbsp;couldn&amp;rsquo;t do anonymous requests, and I&amp;nbsp;didn&amp;rsquo;t want to make authenticated requests for my account from some hacked-together scripts, for fear of triggering some bot detection tripwire that would lock my account.&lt;/p&gt;

&lt;p&gt;So the approach I&amp;nbsp;settled on was to passively collect the requests in the browser, using Safari&amp;rsquo;s Web Inspector, and export them to a HAR file that can be parsed and processed like the data from the public API. (It would be even better to have a browser extension that intercepts XHR calls on twitter.com automatically, but as far I&amp;nbsp;can tell, there is no way for request monitoring extensions to look at the &lt;em&gt;content&lt;/em&gt; of responses, unless you inject scripts to the site.)&lt;/p&gt;

&lt;p class="image"&gt;&lt;a href="/images/posts/social2023/safari_graphql.png"&gt;&lt;img alt="Network tab in Safari Web Inspector, showing requests to HomeTimeline endpoint" src="https://mackuba.eu/images/posts/social2023/safari_graphql.png?1771213129" width="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;initially tried to implement it as a &lt;a href="https://github.com/mackuba/BirdLog"&gt;Mac app&lt;/a&gt;, which gave me a chance to start experimenting with Core Data a&amp;nbsp;bit. But in the end, I&amp;nbsp;rewrote it in Ruby and released it as gem I&amp;nbsp;called “BadPigeon” – named after the friends who visit my balcony every day 🐦&lt;/p&gt;

&lt;p&gt;The gem is designed to output extracted data in the same form as the Twitter API, in a way that can be plugged into the popular &lt;a href="https://github.com/sferik/twitter-ruby"&gt;twitter gem&lt;/a&gt;, so I&amp;nbsp;could use all existing tools I&amp;nbsp;had written with very little changes. The obvious downside is that it needs some manual help with the recording first, but I&amp;nbsp;can live with that. I’ve been using this setup since June and it works pretty well for me so far.&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;had one more Twitter-related project that I&amp;nbsp;sadly had to shut down though – the &lt;a href="https://twitter.com/rails_bot/"&gt;Rails Bot&lt;/a&gt; which has been running non-stop since 2013, mostly unattended, picking and retweeting tweets from some developers in the Ruby community. It requires access to the API&amp;nbsp;to fetch its home timeline periodically from crontab, so I&amp;nbsp;couldn’t make it work this way.&lt;/p&gt;


        &lt;a class="github-card" href="https://github.com/mackuba/bad_pigeon" target="_blank"&gt;
          &lt;h2&gt;&lt;span class="author"&gt;mackuba&lt;/span&gt; ∕ &lt;span class="repo"&gt;bad_pigeon&lt;/span&gt;&lt;/h2&gt;
          &lt;p class="description"&gt;A tool for extracting tweet data from GraphQL requests made by the Twitter website 🐦&lt;/p&gt;
          &lt;img src="https://mackuba.eu/images/github-mark.png?1771213129" class="gh-logo"&gt;
        &lt;/a&gt;
      

&lt;hr /&gt;

&lt;h2&gt;Mastodon’t 🦣&lt;/h2&gt;

&lt;p&gt;As the migration of developer communities out of Twitter started, I&amp;nbsp;was initially &lt;a href="/2022/12/22/social-media-update/"&gt;skeptical&lt;/a&gt;; looking back, I&amp;nbsp;guess I&amp;nbsp;just had to go through the &amp;ldquo;five stages of grief&amp;rdquo; at my own pace… I&amp;nbsp;also didn’t initially see the change as &lt;em&gt;that&lt;/em&gt; bad as some others did, and to be honest I&amp;nbsp;still don’t – to me, Twitter still isn’t literal hell on Earth, it’s just that month after month, it got progressively less useful, less interesting and more annoying.&lt;/p&gt;

&lt;p&gt;So I&amp;nbsp;finally started looking at Mastodon with interest. The idea of the “Fediverse”, a distributed system of many independent servers with a completely open API, where I&amp;nbsp;don’t need to pay absurd prices for an access key, don&amp;rsquo;t have monthly download limits and which can’t be taken over and locked down, was appealing to me.&lt;/p&gt;

&lt;p&gt;You see, I’m a bit crazy about data hoarding and processing information – for many years I’ve been having various ideas about tools I&amp;nbsp;could write to somehow automate finding more relevant content in the noise of social media, to let me waste less time on it while still finding what’s important (the Rails Bot was a very early example of that). So I&amp;nbsp;thought that maybe in this new open world, where the only limit is my imagination, I&amp;nbsp;could build any tools I&amp;nbsp;ever wanted and share them with others.&lt;/p&gt;

&lt;p&gt;Well, turns out it&amp;rsquo;s not that simple…&lt;/p&gt;

&lt;p&gt;It’s true that the Mastodon APIs are completely open and generally permissionless; for example, you can easily download any account’s &lt;a href="https://martianbase.net/api/v1/accounts/109927540589302167/statuses"&gt;complete history&lt;/a&gt; of “toots”, going as far as a few years back, anonymously. The problem is that there is a certain culture of the existing community of the Fediverse that was there way before the great migration, which is extremely against any kind of data collection, archiving and indexing. Making information searchable – information which is broadcasted in the open to the world – is seen as a threat to safety, and anyone who attempts that is labelled a “tech bro”, derided and attacked.&lt;/p&gt;

&lt;p&gt;Sometime in winter I&amp;nbsp;went down the rabbit hole of many, many threads discussing several of such tools, with the authors being attacked and told that they shouldn’t have built them. Just by mentioning in one of the threads that I’m thinking about building a Mac app that allows you to search the history of your home timeline, I&amp;nbsp;got called out on &amp;ldquo;#fediblock&amp;rdquo; (Fediverse&amp;rsquo;s popular channel for warning about bad actors) as someone worthy of blocking.&lt;/p&gt;

&lt;p&gt;All of this has very quickly cured me of any ideas to build pretty much any public tool for the Mastodon API. I&amp;nbsp;just don&amp;rsquo;t have the energy and mental strength to deal with people attacking me this way for simply building tools on an open API&amp;nbsp;that they don&amp;rsquo;t like.&lt;/p&gt;

&lt;p&gt;What I&amp;nbsp;ended up doing though was setting up my own personal Mastodon instance, &lt;a href="https://martianbase.net"&gt;martianbase.net&lt;/a&gt;. I&amp;nbsp;joked that I&amp;rsquo;m probably the only person who hates Mastodon and also has their own Mastodon instance&amp;hellip; But the first instance I&amp;nbsp;signed up on last year was shut down unexpectedly, giving me no chance to migrate the account. That’s another thing I&amp;nbsp;dislike about the ActivityPub system – your account identity and data is bound to the domain of your instance, and there is no easy way out if your admin misbehaves or disappears, or just has a different view on which other servers you should be able to talk to. So at that point I&amp;nbsp;decided not to trust another instance admin, but to set up my own place, so that I&amp;nbsp;can have full control over it.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;Blue skies ahead 🌤&lt;/h2&gt;

&lt;p&gt;And then, just as Twitter was slowly going down and Mastodon has disappointed me – I&amp;nbsp;started hearing about &lt;a href="https://blueskyweb.xyz"&gt;Bluesky&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Started as an idea of Jack Dorsey from Twitter &lt;a href="https://www.cnbc.com/2019/12/11/twitter-ceo-jack-dorsey-announces-bluesky-social-media-standards-push.html"&gt;back in 2019&lt;/a&gt;, with a goal of building a “decentralized Twitter” that Twitter itself could possibly one day be a part of, the project has been going on for a few years, and just as the whole Twitter chaos started, Bluesky got to the point where it could be presented to the world.&lt;/p&gt;

&lt;p&gt;(Important note here, since media has widely promoted Bluesky as “Jack’s social network” and his name puts a lot of people off: it’s not in fact Jack’s social network. He’s not the CEO (a woman named &lt;a href="https://www.forbes.com/sites/digital-assets/2023/04/25/twitter-hatchling-bluesky-emerges-from-its-shell/"&gt;Jay Graber&lt;/a&gt; is), he does not manage or control the company, and AFAIK he’s actually been very little involved in it recently, having mostly switched his interest to &lt;a href="https://nostr.com"&gt;Nostr&lt;/a&gt; – to the point that he has even deleted his Bluesky profile.)&lt;/p&gt;

&lt;p&gt;The attention and interest that Bluesky has received after lauching an invite-only beta has widely exceeded the team’s expectations, but this was both a blessing and a curse. They weren’t really prepared to run a real Twitter competitor that could accept the “refugees” escaping Elon’s playground. The thing is, they were mostly focused on the underlying protocol before, and the site itself has been launched as a bit of a demo. A lot of things that people consider pretty essential in a social networking site weren’t ready. But the team – which at that point was less than 10 developers in total, AFAIK – started adapting to the new reality, working as hard as they could to make the site usable for much larger crowds that they had been planning to.&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;got access to Bluesky in late April. I&amp;nbsp;don’t want to get into too much detail here about what it’s like, how it’s evolved since then and so on – I’m going to write a few more blog posts about Bluesky specifically. But long story short, I&amp;nbsp;was completely hooked from day one.&lt;/p&gt;

&lt;p&gt;Yes, it’s invite-only, has a much smaller userbase than the Fediverse, it’s an early beta, it doesn’t have videos, gifs or even hashtags. The iOS/Swift developers there are as rare as a unicorn. But it has a really nice community of users, third party developers who hack on various tools and help each other, and team members who interact with us on the site all the time. It looks and feels more like Twitter than Mastodon does, and it somehow just feels more fun to be on.&lt;/p&gt;

&lt;p&gt;But the thing that excites me the most is the &lt;a href="https://atproto.com"&gt;AT Protocol&lt;/a&gt; it’s built on and its potential. It’s a completely open federated protocol, just like ActivityPub that Mastodon uses, and it’s intended to eventually create another “fediverse” of distributed social apps (though the federation part is not live yet, but coming soon). It’s designed to take some lessons from what doesn’t work well in ActivityPub (and would be hard to change) and design the architecture better. For example, it uses &amp;ldquo;Decentralized IDs&amp;rdquo; (DID) independent of the hosting server to identify accounts, which makes it easy to migrate accounts between servers (and your handle can be any domain name you own, like &lt;a href="https://bsky.app/profile/mackuba.eu"&gt;@mackuba.eu&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The code it’s running on is &lt;a href="https://github.com/bluesky-social/"&gt;open source&lt;/a&gt;, the APIs are completely open, and it all just invites you to write some tools and libraries for it – to be the first person to write a Ruby library, a Swift SDK, a command line client, a website with statistics. To be the first to plant a flag where others will come later.&lt;/p&gt;

&lt;p&gt;I’ve been spending most of the time since April working on one Bluesky-related project after another, sometimes switching between a few in parallel. In the rest of this blog post, I&amp;nbsp;wanted to show you some of the things I’ve been busy building:&lt;/p&gt;

&lt;h3&gt;Minisky&lt;/h3&gt;

&lt;p&gt;On the first day after I&amp;nbsp;got in, I&amp;nbsp;already started digging in the API&amp;nbsp;and I&amp;nbsp;wrote a small &lt;a href="https://gist.github.com/mackuba/ddcb225ae4e6cf08e0e0396b3f6a2f6d"&gt;Ruby script&lt;/a&gt; for archiving my timeline and likes (of course I&amp;nbsp;did…). This eventually evolved into a Ruby gem I&amp;nbsp;called Minisky, which provides a minimalistic API&amp;nbsp;client that handles logging in, refreshing access tokens, making GET and POST requests to the API&amp;nbsp;and returning parsed JSON responses.&lt;/p&gt;

&lt;p&gt;It doesn’t include any higher-level features like “get posts”, you have to know the name of the endpoint, what params to pass and what fields it returns, but it handles all the basic boilerplate for you. I&amp;nbsp;use it as a base for some internal scripts, and for manually getting or sending some data to the API&amp;nbsp;in the Ruby console. If you want to start playing with the Bluesky API&amp;nbsp;or build some more specific tool that uses it, you can give this library a try (see the &lt;a href="https://github.com/mackuba/minisky/tree/master/example"&gt;example&lt;/a&gt; folder for some ideas). It has no dependencies apart from Ruby stdlib.&lt;/p&gt;


        &lt;a class="github-card" href="https://github.com/mackuba/minisky" target="_blank"&gt;
          &lt;h2&gt;&lt;span class="author"&gt;mackuba&lt;/span&gt; ∕ &lt;span class="repo"&gt;minisky&lt;/span&gt;&lt;/h2&gt;
          &lt;p class="description"&gt;A minimal client of Bluesky/AtProto API&lt;/p&gt;
          &lt;img src="https://mackuba.eu/images/github-mark.png?1771213129" class="gh-logo"&gt;
        &lt;/a&gt;
      

&lt;h3&gt;Custom feeds on Bluesky&lt;/h3&gt;

&lt;p&gt;Bluesky has a really cool feature that I&amp;nbsp;think is pretty unique among all the social networks. On social sites, you normally have either a reverse-chronological timeline of posts from the people you follow, or some kind of algorithmic “home” feed that mixes them up with other suggested posts, in a way that you usually don’t fully understand and may not like (or both of these feeds).&lt;/p&gt;

&lt;p&gt;Bluesky has both of these, but it also lets &lt;em&gt;anyone&lt;/em&gt; build a custom feed that selects and orders posts however you like, and most importantly, lets you make this feed available to everyone else. &lt;a href="https://blueskyweb.xyz/blog/7-27-2023-custom-feeds"&gt;Custom feeds&lt;/a&gt; are a core feature of the app; it lets you browse popular feeds from other people, feeds are listed in a separate tab on the feed author’s profile, and you can “pin” the feeds you use often, which puts them in the top bar in the mobile app, as if it was another built-in timeline. People build all kinds of feeds – thematic feeds like various scientific or art or NSFW feeds, feeds for specific communities like &amp;ldquo;Blacksky&amp;rdquo;, general “top posts this week” feeds, or different variations of an algorithmic “home feed” using various approaches.&lt;/p&gt;

&lt;p&gt;The way the feeds work is that you need to provide an HTTP service on your server which implements a couple of endpoints. The Bluesky server then makes a request to your service on user’s behalf when they want to view the feed, and your service should respond with a JSON that includes a list of post URIs. Bluesky then takes these URIs and turns them into full post JSONs that it returns to the client.&lt;/p&gt;

&lt;p&gt;When the team launched this feature back in May, they included a sample &lt;a href="https://github.com/bluesky-social/feed-generator/"&gt;feed service project&lt;/a&gt; implemented in TypeScript. But I’m not a big fan of JS/TS and Node, so of course I&amp;nbsp;had to reimplement it all in Ruby&amp;nbsp;:]&lt;/p&gt;

&lt;p&gt;I’ve spent quite a lot of time working on the feeds and related code this summer, and the result of this is three separate Ruby projects that I’ve open sourced on GitHub (in addition to my main project which is private).&lt;/p&gt;

&lt;h3&gt;BlueFactory&lt;/h3&gt;

&lt;p&gt;The first part is an implementation of the feed service itself. I&amp;nbsp;based it on Sinatra, and it implements the three API&amp;nbsp;endpoints required from a feed service. You need to provide some configuration (hostname, DID of the owner etc.) and your custom class to call back to in order to get the list of post URIs and the feed metadata. If you want, you can further customize the server using the Sinatra API, e.g. adding some custom routes with HTML content.&lt;/p&gt;

&lt;p&gt;Feeds can generally be divided into two categories: general and thematic feeds that return the same content for everyone, and personalized feeds that show the feed from a specific user’s perspective. The latter are usually much more complicated to build, since you will often need much more data of different kinds to generate the response, depending on your algorithm. If you want to build a personalized feed, the request includes a &lt;a href="https://jwt.io"&gt;JWT token&lt;/a&gt; that you can use to get the requesting user’s DID, and the gem can pass that as a param to your class (although note that at the moment it does not verify the token, so it can be easily faked).&lt;/p&gt;


        &lt;a class="github-card" href="https://github.com/mackuba/blue_factory" target="_blank"&gt;
          &lt;h2&gt;&lt;span class="author"&gt;mackuba&lt;/span&gt; ∕ &lt;span class="repo"&gt;blue_factory&lt;/span&gt;&lt;/h2&gt;
          &lt;p class="description"&gt;A simple Ruby server using Sinatra that serves Bluesky custom feeds&lt;/p&gt;
          &lt;img src="https://mackuba.eu/images/github-mark.png?1771213129" class="gh-logo"&gt;
        &lt;/a&gt;
      

&lt;h3&gt;Skyfall&lt;/h3&gt;

&lt;p&gt;To return the post URIs from the feed service, first you need to get the posts from somewhere. You could possibly get them from the API, but realistically, a much better option is to connect to a so-called “firehose” web service and stream and save them as they are created, keeping a copy in a local database.&lt;/p&gt;

&lt;p&gt;The firehose streams every single thing happening on the network, live – every new and deleted post, follow, like, block, and so on. Depending on your specific feed idea, you will usually only need to keep a small fraction of this data, e.g. only posts and only those that match some regexps – but you need to parse it all first to know what to keep. What further complicates things is that the firehose data does not come in a JSON form, but instead uses a bunch of binary protocols originated from &lt;a href="https://ipld.io"&gt;IPLD&lt;/a&gt;/&lt;a href="https://ipfs.tech"&gt;IPFS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The second Ruby gem is meant to simplify this for you. It uses an existing &lt;a href="https://github.com/cabo/cbor-ruby/"&gt;CBOR&lt;/a&gt; library to do some of the binary protocol parsing and &lt;a href="https://github.com/faye/faye-websocket-ruby/"&gt;faye-websocket&lt;/a&gt; for the websocket connection. It connects to the firehose websocket on a given hostname and returns parsed message objects with the info about specific add/remove operations and relevant JSON records.&lt;/p&gt;

&lt;p&gt;The firehose (and the Skyfall gem) isn’t only useful for creating feed services – you could possibly use it for any other project that needs to track some kind of records from the network in real time, whether it’s follows (to create a &lt;a href="https://bsky.jazco.dev"&gt;connection graph&lt;/a&gt; of the whole network, or to track when a follower unfollows you), or blocks (to find out &lt;a href="https://bsky.thieflord.dev"&gt;who is blocking you&lt;/a&gt;), or to monitor when you or your company or project are mentioned by anyone anywhere. I’ve also included an &lt;a href="https://github.com/mackuba/skyfall/tree/master/example"&gt;examples&lt;/a&gt; folder with some sample scripts in the repo.&lt;/p&gt;


        &lt;a class="github-card" href="https://github.com/mackuba/skyfall" target="_blank"&gt;
          &lt;h2&gt;&lt;span class="author"&gt;mackuba&lt;/span&gt; ∕ &lt;span class="repo"&gt;skyfall&lt;/span&gt;&lt;/h2&gt;
          &lt;p class="description"&gt;A Ruby gem for streaming data from the Bluesky/AtProto firehose&lt;/p&gt;
          &lt;img src="https://mackuba.eu/images/github-mark.png?1771213129" class="gh-logo"&gt;
        &lt;/a&gt;
      

&lt;h3&gt;Bluesky feeds template&lt;/h3&gt;

&lt;p&gt;This project puts the previous two together and combines them into an example of a complete Bluesky feed service, which reads posts from the firehose, saves them to an SQLite database and serves them on a required endpoint – basically a reimplementation of the official TypeScript example in Ruby.&lt;/p&gt;

&lt;p&gt;This is a “template” repo, which means it’s not meant to be used as-is, but instead forked and modified in your own copy. The reason is there are simply too many things that you may want to do differently – deployment method, chosen database, specific data to keep etc., and making this all configurable would be an impossible task. Instead, I’ve extracted the “input” and “output” parts as separate gems that can be used directly, and you build the parts in the middle – but you can use this template project as a good starting point.&lt;/p&gt;

&lt;p&gt;My own feed service project is a private repo, but I’m keeping it in a similar structure to this template and I’m manually backporting some fixes and new features from time to time.&lt;/p&gt;


        &lt;a class="github-card" href="https://github.com/mackuba/bluesky-feeds-rb" target="_blank"&gt;
          &lt;h2&gt;&lt;span class="author"&gt;mackuba&lt;/span&gt; ∕ &lt;span class="repo"&gt;bluesky-feeds-rb&lt;/span&gt;&lt;/h2&gt;
          &lt;p class="description"&gt;Template of a custom feed generator service for the Bluesky network in Ruby&lt;/p&gt;
          &lt;img src="https://mackuba.eu/images/github-mark.png?1771213129" class="gh-logo"&gt;
        &lt;/a&gt;
      

&lt;h3&gt;My feeds&lt;/h3&gt;

&lt;p&gt;And now we get to the part that all of this was for – building my own custom feeds.&lt;/p&gt;

&lt;p&gt;I&amp;nbsp;mentioned earlier that I&amp;nbsp;often think about and experiment with various ways to find most relevant content to me on social media. So when I&amp;nbsp;heard about the custom feeds feature, I&amp;nbsp;immediately had an idea to build a feed for Mac/iOS developers that filters only posts on this topic, using a long list of keywords and regexps (I’ve actually reused a lot of work I’ve done a while ago for an unfinished thing I&amp;nbsp;played with on Twitter).&lt;/p&gt;

&lt;p&gt;It took me a couple of months to build all the pieces of the “feed generator”, but I’ve launched the Apple Dev feed in July. It isn’t very busy so far, to put it mildly, because there still aren’t that many iOS devs on Bluesky 😅 But as of today, it has 35 likes – only 50 likes less than the &lt;em&gt;other&lt;/em&gt; &lt;a href="https://bsky.app/profile/did:plc:kcevumnk4gjxyegqwbubpajo/feed/taylor-swift"&gt;Swift feed&lt;/a&gt;&amp;nbsp;:]&lt;/p&gt;

&lt;p&gt;Apart from the iOS dev feed, I’ve also made a more general macOS users feed and a couple of other feeds that were mostly a proof of concept / playground while building the service, but a lot of people seem to find them useful anyway, so I&amp;rsquo;ve left them running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/apple"&gt;iOS &amp;amp; Mac Developers feed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/mac"&gt;macOS users feed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/linux"&gt;Linux feed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/starwars"&gt;Star Wars feed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr/feed/build"&gt;#buildinpublic feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;Skythread&lt;/h3&gt;

&lt;p&gt;The last one of my Bluesky-related projects, also with a “sky” in the name (most of the &lt;a href="https://atproto.com/community/projects"&gt;third party projects&lt;/a&gt; so far have either the word “blue” or “sky” as part of the name 😄). And this one is written in JavaScript for a change.&lt;/p&gt;

&lt;p&gt;If you use Twitter and/or Mastodon a lot, you probably have the experience of reading some complicated thread and getting lost, not knowing who replies to whom or if you haven’t missed a whole part of the discussion. These two display branching out threads a bit differently – Twitter hides some of the branches, while Mastodon shows all direct and indirect replies in one flat list. In both cases, it’s not a perfect solution for reading some heated “&lt;a href="https://knowyourmeme.com/memes/events/hellthread-hellrope-bluesky"&gt;hellthreads&lt;/a&gt;” that branch out endlessly. For me, a UI&amp;nbsp;more like the one on Reddit would be ideal. (Bluesky has recently a thread view with limited nesting, as an experimental feature.)&lt;/p&gt;

&lt;p&gt;So that’s what I’ve built, as a web tool. You enter a URL of the root of the thread on bsky.app, and it renders the whole thread as a tree. You can use the +/– buttons to collapse and expand parts of the tree, just like on Reddit, and if you log in, you can also click the heart icons below a comment to like it:&lt;/p&gt;

&lt;p class="image"&gt;&lt;a href="/images/posts/social2023/skythread.png"&gt;&lt;img alt="View of a thread with 3 posts, nested under one another" src="https://mackuba.eu/images/posts/social2023/skythread.png?1771213129" width="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The initial version of Skythread required logging in first to see anything, but I’ve recently switched it to a different API&amp;nbsp;that allows me to load whole threads without an access token. Note that the official Bluesky web app currently does not allow viewing any content unauthenticated – just like Twitter after the recent changes – so tools like Skythread, and other similar ones (e.g. &lt;a href="https://skyview.social"&gt;Skyview&lt;/a&gt;) are the only way right now to share links to posts and threads with people who don’t have an account; but this is a temporary situation and Bluesky should be open to the world (for reading at least) in near future.&lt;/p&gt;


        &lt;a class="github-card" href="https://github.com/mackuba/skythread" target="_blank"&gt;
          &lt;h2&gt;&lt;span class="author"&gt;mackuba&lt;/span&gt; ∕ &lt;span class="repo"&gt;skythread&lt;/span&gt;&lt;/h2&gt;
          &lt;p class="description"&gt;Thread viewer for Bluesky&lt;/p&gt;
          &lt;img src="https://mackuba.eu/images/github-mark.png?1771213129" class="gh-logo"&gt;
        &lt;/a&gt;
      

&lt;hr /&gt;

&lt;h2&gt;One app to rule them all&lt;/h2&gt;

&lt;p&gt;So where can you find me now on social media? As you might have guessed from the earlier sections, I’m spending most of the time on Bluesky now; which may be a bit strange, because that’s not where most of my friends and follows from Twitter ended up. A large part of the iOS/Mac/Swift programming community has moved to Mastodon and stayed there, with some stubbornly sticking to Twitter or posting to both. Possibly also to Threads, which I&amp;nbsp;don’t even have access to.&lt;/p&gt;

&lt;p&gt;But there’s something about Bluesky and the AT Protocol that really draws me to it… I&amp;nbsp;think it&amp;rsquo;s some combination of a nicer UI/UX, tech/architecture that I&amp;nbsp;like more, a new community that is only just forming, and having this feeling like I&amp;rsquo;m blazing the trail, being able to build all the tooling that doesn&amp;rsquo;t exist yet. I&amp;nbsp;like being part of something that&amp;rsquo;s being created around me, flying on that plane that&amp;rsquo;s &lt;a href="https://www.youtube.com/watch?v=UZq4sZz56qM"&gt;being built in the air&lt;/a&gt;, watching the devs build it live and feeling like I&amp;rsquo;m part of it all. I&amp;nbsp;enjoy being there, I&amp;nbsp;really want it to succeed, and I&amp;nbsp;want to help with that as much as I&amp;nbsp;can.&lt;/p&gt;

&lt;p&gt;So I&amp;nbsp;have friends on all three platforms, and even though I&amp;nbsp;spend most time on Bluesky, I&amp;nbsp;check all three everyday, for slightly different content – Twitter for news, Mastodon for Swift programming, Bluesky for… dopamine? And since some people only follow me here and some only there, I&amp;nbsp;end up manually cross-posting a lot of things to 2 or 3 websites.&lt;/p&gt;

&lt;p&gt;Wouldn’t it be nice to have a tool, kind of like &lt;a href="https://buffer.com"&gt;Buffer&lt;/a&gt;, that can let you post to Twitter, Mastodon and/or Bluesky in parallel? There doesn’t seem to be, so I’ve decided to build one myself&amp;nbsp;:] This one isn’t available yet and it still needs a lot of work before I&amp;nbsp;can call it an “MVP”, but it’s going to look something like this:&lt;/p&gt;

&lt;p class="image noborder"&gt;&lt;img alt="A small New Post window with an avatar and 3 network icons on the left, Post button in the bottom right, and the text &amp;lsquo;Hack the planet!&amp;rsquo; in the main text area" src="https://mackuba.eu/images/posts/social2023/new_post_window.png?1771213129" width="520"&gt;&lt;/p&gt;

&lt;p&gt;In the meantime, you can follow me here on any of these platforms – listed in the order of preference&amp;nbsp;:)&lt;/p&gt;

&lt;ul class="social-media"&gt;
  &lt;li class="bluesky"&gt;&lt;span&gt;🦋&lt;/span&gt; Bluesky: &lt;a href="https://bsky.app/profile/mackuba.eu"&gt;@mackuba.eu&lt;/a&gt;&lt;/li&gt;
  &lt;li class="mastodon"&gt;&lt;span class="mamutek"&gt;🦣&lt;/span&gt; Mastodon: &lt;a href="https://martianbase.net/@mackuba"&gt;mackuba@martianbase.net&lt;/a&gt;&lt;/li&gt;
  &lt;li class="twitter"&gt;&lt;img alt="" src="https://mackuba.eu/images/twitter-logo.png?1771213129" width="20"&gt; Twitter: &lt;a href="https://twitter.com/kuba_suder"&gt;@kuba_suder&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</content>
  </entry>
  <entry>
    <id>https://mackuba.eu/2022/12/22/social-media-update/</id>
    <title>Social media update - Elon's Twitter and Mastodon</title>
    <published>2022-12-22T16:21:01Z</published>
    <updated>2022-12-22T16:21:01Z</updated>
    <link href="https://mackuba.eu/2022/12/22/social-media-update/"/>
    <content type="html">&lt;p&gt;&lt;strong&gt;Update 01.03.2023&lt;/strong&gt;: Updated Mastodon address - my previous instance has been unexpectedly shut down and I&amp;nbsp;had to make a new account. I&amp;rsquo;ve decided to set up my own server to make sure it won&amp;rsquo;t happen again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update 09.11.2023&lt;/strong&gt;: I&amp;nbsp;made a &lt;a href="http://localhost:3000/2023/11/09/year-of-social-media-coding/"&gt;follow-up post&lt;/a&gt; which talks about the social media related projects I&amp;rsquo;ve been working on this year, and about Bluesky, where I&amp;rsquo;m spending most of the time now.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;This is just a small update about Twitter and Mastodon, since things have been… very unstable and chaotic in the last few weeks, as you&amp;rsquo;ve surely noticed if you log in to these even occassionally.&lt;/p&gt;

&lt;p&gt;Twitter has been my internet home for over 13 years now. I&amp;nbsp;started using it when my colleagues from &lt;a href="https://lunarlogic.io"&gt;Lunar Logic&lt;/a&gt; showed it to me, and especially in the recent years it&amp;rsquo;s been my main source of information and news. It&amp;rsquo;s where I&amp;nbsp;went to keep track of what was happening in the Apple/Swift world, find useful tips about UIKit, SwiftUI&amp;nbsp;or Xcode, follow the news, rumors and dramas on the Crypto Twitter, and find out every day what important thing was happening in the world, including following the &lt;a href="/2020/04/03/coronavirus-charts/"&gt;Covid pandemic&lt;/a&gt; and the Russian invasion of Ukraine this year.&lt;/p&gt;
&lt;p&gt;Like most of the people I&amp;rsquo;m following, I&amp;rsquo;m not very happy about Elon&amp;rsquo;s takeover and his actions, how he randomly makes changes to the rules based on his current mood, blocks journalists who write about him and how he fired or scared away most of the people who kept the site working. I&amp;rsquo;m worried about how the future looks for the platform, if Twitter will even exist in this form a year or two from now. I&amp;nbsp;wish this all hadn&amp;rsquo;t happened, and I&amp;rsquo;m angry at the people who made it happen for their own gain.&lt;/p&gt;

&lt;p&gt;But so far, Twitter is still working and is still a great place to get the news, tips and information about so many things. I&amp;rsquo;m not ready to give up on this site as long as the feed is loading and there are some tweets left to read.&lt;/p&gt;

&lt;p&gt;I&amp;rsquo;ve been trying out Mastodon like everyone else and I&amp;nbsp;slowly get more comfortable there, but it still feels a bit alien to me. It feels like when you move in to a new apartment and everything is different there than you&amp;rsquo;re used to, some things are missing, some things are better, some things are worse than in your old place, but a lot of your subconscious habits and muscle memory stop working. I&amp;nbsp;had some ways of using Twitter that worked for me and a bunch of private tools I&amp;nbsp;wrote for myself to help me automate some things - I&amp;nbsp;will have to figure this all out again now.&lt;/p&gt;

&lt;p&gt;So I&amp;nbsp;am on Mastodon, if only because I&amp;nbsp;don&amp;rsquo;t want to miss out on things, but I&amp;nbsp;am still on Twitter and I&amp;rsquo;m planning to stay and keep posting there, as long as it stays usable. I&amp;nbsp;will probably be posting more on Twitter than Mastodon, because I&amp;nbsp;feel more comfortable there. I&amp;nbsp;hope some of my friends and people I&amp;nbsp;follow stay on the platform, or at least check in from time to time.&lt;/p&gt;

&lt;p&gt;So here&amp;rsquo;s where you can find and follow me (&lt;strong&gt;updated 09.11.2023&lt;/strong&gt;):&lt;/p&gt;

&lt;ul class="social-media"&gt;
  &lt;li class="bluesky"&gt;&lt;span&gt;🦋&lt;/span&gt; Bluesky: &lt;a href="https://bsky.app/profile/mackuba.eu"&gt;@mackuba.eu&lt;/a&gt;&lt;/li&gt;
  &lt;li class="mastodon"&gt;&lt;span class="mamutek"&gt;🦣&lt;/span&gt; Mastodon: &lt;a href="https://martianbase.net/@mackuba"&gt;mackuba@martianbase.net&lt;/a&gt;&lt;/a&gt;&lt;/li&gt;
  &lt;li class="twitter"&gt;&lt;img alt="" src="https://mackuba.eu/images/twitter-logo.png?1771213129" width="20"&gt; Twitter: &lt;a href="https://twitter.com/kuba_suder"&gt;@kuba_suder&lt;/a&gt;&lt;/li&gt;
  &lt;li class="rss"&gt;&lt;img alt="" src="https://mackuba.eu/images/rss.png?1771213129" width="16"&gt; if you like to use RSS, check out the &lt;a href="/feeds"&gt;feeds&lt;/a&gt; for my blog&lt;/li&gt;
&lt;/ul&gt;



</content>
  </entry>
</feed>
