Standard Reader
MacKuba blog
cocoa

MacKuba blog

Kuba Suder's blog on Mac/iOS & web dev

@mackuba.eu2readers76posts4mo ago
LatestRecent writing
Running Bluesky PDS undockered
Feb 4, 2026
A bit over a year ago, in the first week of January 2025, I migrated my main Bluesky account to my own PDS on a Netcup VPS. It’s been quite easy to set up using the official installer, and it’s been running pretty much without any problems or maintenance the whole year. Despite that, I haven’t been 100% happy with this setup for one reason: Docker. So I decided to try to take it out of the box, and I 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 did this, if you’re interested. There are a few existing posts about this already: https://benharri.org/bluesky-pds-without-docker/ https://char.lt/blog/2024/10/atproto-pds/ https://cprimozic.net/notes/posts/notes-on-self-hosting-bluesky-pds-alongside-other-services/ But I figured it doesn’t hurt to make another one that does things slightly differently again. “There are many like this, but this one is mine”. (I mostly followed the benharri.org version.) Note: I’m describing what I did to migrate an existing PDS from in-Docker to outside-Docker, so I already had existing data and pds.env config; if you wanted to install one from scratch this way, you’d probably need to also set up the config manually. You might be asking: why? And that’s a good question. I mostly wouldn’t recommend this setup over the standard Docker one by default, unless you know what you’re doing. The standard installation is literally running one command and answering some questions, and then it auto-updates and manages everything. My reason is that I’m generally pretty familiar with installing things on Linux servers manually, but I’m completely unfamiliar with Docker. I always wanted to do some modifications on the PDS, but I didn’t know how, because the Docker setup basically takes over the whole server for itself. I don’t know where it pulls code from, I don’t know where it puts it, and I don’t know when it can overwrite any changes I make. I don’t feel in control. (And to be clear, this is likely a me problem.) So here’s what I did (this setup is for Ubuntu 24.04 Noble): Install Nginx The standard PDS distribution uses Caddy, but I use Nginx everywhere and I have configs built for it, so I’ve set up Nginx: # 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 Also here’s a standard thing I do on VPSes to let me install webapps in /var/www from my account: # set up environment for webapps sudo groupadd deploy sudo adduser psionides deploy sudo chown root:deploy /var/www sudo chmod 775 /var/www I also need Certbot for LetsEncrypt: # install Certbot sudo apt-get install --no-install-recommends certbot python3-certbot-nginx sudo certbot plugins --nginx --prepare certbot plugins --nginx --prepare does some initial setup of …
Social
ATProto blog posts collection
Nov 18, 2025
(Last update: 3 Jun 2026.) I 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 Leaflet got popular in these circles) that I started using an RSS reader again just to keep up with everything. 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’s not always easy to dig it out from the archive. I thought it would be nice to have one place collecting those old and newer blog posts to make them easier to find. So I went through those RSS feeds, my like archives and other places, and collected everything I could find here in an organized list. I also included the documents from the “Proposals” GitHub repo, and various posts from the “Discussions” section in the ATProto repo. This is a subjective selection – from many blogs I skipped some less relevant posts or only included a couple out of many – so if you’re interested, click through to the home page from any post and look for the other posts there. Search posts by title: Bluesky official sources atproto.com articles Atproto for distributed systems engineers (Sep 2024) Atproto Ethos (Apr 2025) bsky.social/about/blog (non-technical blog) Composable Moderation (Apr 2023) How to verify your Bluesky account (Apr 2023) Federation Architecture Overview (May 2023) Bluesky: An Open Social Web (Feb 2024) Bluesky’s Stackable Approach to Moderation (Mar 2024) Tips and Tricks for Bluesky Search (May 2024) Bluesky Welcomes Mike Masnick to Board of Directors (Aug 2024) Bluesky Announces Series A to Grow Network of 13M+ Users (Oct 2024) 2024 In Review (Dec 2024) Bluesky’s Patent Non-Aggression Pledge (Oct 2025) What’s Next at Bluesky (Jan 2026) A New Chapter for Bluesky (Mar 2026) Bluesky’s 2025 $100M Series B Lays Foundation for Open Social Web (Mar 2026) docs.bsky.app/blog (old dev blog) Click to expand 2023 Why are blocks on Bluesky public? (Jun 2023) Featured Community Project: Skyfeed (Aug 2023) Posting via the Bluesky API (Aug 2023) Updates to Repository Sync Semantics (Aug 2023) Rate Limits, PDS Distribution v3, and More (Sep 2023) Bluesky BGS and DID Document Formatting Changes (Oct 2023) 2023 Protocol Roadmap (Oct 2023) Download and Parse Repository Exports (Nov 2023) Featured Community Project: Bridgy Fed (Dec 2023) 2024 Early Access Federation for Self-Hosters (Feb 2024) Announcing AT Protocol Grants (Mar 2024) Bluesky’s Moderation Architecture (Mar 2024) Meet the second batch of AT Protocol Grant Recipients (Apr 2024) 2024 Protocol Roadmap (May 2024) Labeling Services Microgrants (May 2024) Typescript API Package Auth Refactor (Aug 2024) OAuth for AT Protocol (Sep 2024) Lexicons, Pinned Posts, and Interopera…
Social
How I ran one Ruby app on three SQL databases for six months
Oct 15, 2025
Since June 2023, I’ve been running a service written in Ruby (Sinatra) that provides several Bluesky custom feeds (initially built with a feed for the iOS/Mac developers community 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 endpoint. The Bluesky AppView (API server) then calls your service passing some request parameters, and your service responds with a list of URIs of posts (which the API 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’re interested, check out my example feed service project.) In order to provide such service, in practice you need to connect to the Bluesky “firehose” streaming API 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 chose the latter, since that lets me retry the matching at any time after I modify the keyword lists and see what would be added after that change (and also some of the feeds I now run require having all posts). I also use the same database/service to generate e.g. the total daily/weekly stats here. 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 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. In addition to that, the firehose stream (if you use the original CBOR stream from a relay, not Jetstream, 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.) On disk, the millions of posts per day add up to a few gigabytes per day. Since I was running this on a VPS with a 256 GB disk (Netcup, RS 1000 – reflink), I 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 don’t really need to keep the older posts …
RubyDatabases
Introduction to AT Protocol
Aug 20, 2025
Walkthrough of the various parts and concepts in Bluesky's AT Protocol (ATProto), the types of servers involved and how it all fits together
Social
Social media update 2025
Jun 30, 2025
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. 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. Myself, I’ve basically left Twitter; I 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. So where do we go from here? Obviously everyone has their own take on that, this is just mine. But I 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 “Connected places”. That means Bluesky and Mastodon/Fediverse (with emphasis on “and”), 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 aren’t very serious about it). My advice: → If you’re currently cross-posting or bridging between Mastodon and Bluesky: awesome! ❤️ → If you’re active on Mastodon, but currently ignoring or forgot about Bluesky: please reconsider it. I know that these two communities have a lot of differences between them, and we love to hate each other (I fully admit I’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 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. 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: there are some native apps which let you post to two or more services in parallel, e.g. Croissant, Openvibe or SoraSNS most social media management services like Buffer 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. e.g. Fedica was one of the ones that had support for Bluesky from very early on also my friend from my first job, Peter Solnica (known from some Ruby libraries like DataMapper/ROM, dry-rb, Hanami, and now some Elixir libs too) is building his own called JustCrossPost I use a little tool in Ruby I wrote for myself named …
GeneralSocial
Micro.blog journal
Feb 4, 2025
Update 17.11.2025: I’ve migrated this journal blog now from Micro.blog to Leaflet, a new blogging service built on top of Bluesky’s ATProto. I wrote about this here, the new URL is https://lab.mackuba.eu (I’ll add a redirect from ‘journal’ later). Just a quick update, if you’re following this blog via RSS: I’ve started a separate “journal” blog on micro.blog: journal.mackuba.eu. Micro.blog is an interesting service: it’s a one-man indie business that’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’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 understand, is to use a network of blogs to build a social network that uses the web itself as the foundation. I’m not really planning to use it in this social network mode, since I’m pretty happy now posting on Bluesky and to limited degree on Mastodon (I’ve completely stopped posting on Twitter at this point, since last autumn, when Elon started openly supporting Trump). I’m also not completely sold on this “web as a social network” idea. And I don’t intend it to replace this blog here either – I will still be (very) occasionally posting those super long articles here like the one about NSButtons or the guide to Bluesky. But I’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 “mediumblog” so to say (not to be confused with a Medium blog) – like this post, for example. Something where I can sometimes post my thoughts more easily, when I want to write something that doesn’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. It’s also nice that it’s supposed to sync replies from Bluesky/Mastodon under the posted link back to the blog page as comments below (I’ll try to implement the same thing here). I also have it configured with my own domain, so I can possibly migrate it to something self-hosted like Jekyll or Hugo at some point, keeping all the links and content. For now, I’ve posted two updates about what I’ve been working on recently: about tuning a Postgres database to which I’m trying to migrate my Bluesky feeds service and a review of all I’ve done in 2024 I don’t know how often I will end up posting there, I don’t want to pressure myself, just to have a place to post when I have a need. So if you’re curious, follow me there via RSS (or on Bluesky or Mastodon).
GeneralSocial
March 2024 projects update
Mar 27, 2024
I’ve been still pretty busy with various Bluesky- and social-related projects recently, so here’s a small update on what I’ve been working on since my November post, if you’re interested: Skythread – quote & hashtag search I was missing one useful feature that’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’t currently collect and expose this info, so it’s not a simple matter of calling the API. But since everything’s open, anyone can build a service that does this, they just need to collect the data themselves. Since I’m already recording all recent posts in a database for the purposes of feeds and other tools, I figured I could just add an indexed quote_id 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. Skythread, my thread reading tool, seemed like a good place to add a UI 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, like this (it doesn’t currently do pagination though). You can open that page directly by appending the bsky.app URL of a post after the quotes= parameter here: https://blue.mackuba.eu/skythread/?quotes=. In the same way, I also indexed posts including hashtags, since hashtags were being written into post records since the autumn, but it wasn’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’t need to use Skythread for that. I hope that the quote search also won’t be needed for much longer :) Handles directory 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’ve used as your handle. So for official accounts like The New York Times, The Washington Post, or Alexandria Ocasio-Cortez, it’s enough if they just set their handle to their main website domain (or a subdomain of house.gov in AOC’s case) to prove they’re legit – they don’t need to apply anywhere to get a blue or gold tick on their profile. I was thinking one day that it would be nice to see how many e.g. .gov handles there are and notice easily when new ones show up. So I grabbed a list of all custom handes from the plc.directory and started recording new and updated ones from the firehose. In the end, I decided to build a whole catalog of all custom handles, grouped by TLD, and show which TLDs are the most popular. At first I only included the “traditional” main TLDs and country domains, but a lot of people liked it and I got a lot of requests to also include domains like .art, .blue, .xyz and so on, so in the next update I’ve add…
RubyFrontend
A complete guide to Bluesky 🦋
Feb 21, 2024
I've decided to write down some of the tips & tricks that I often give to friends when I send them an invite code, or the advice and answers that I sometimes give to people that I find in some feed asking about things. This of course got much longer than I planned 😅
Social
2023: Year of social media coding
Nov 9, 2023
I had different plans for this year… then, Elon Musk happened. Elon took over Twitter in October last year, which set many different processes in motion. A lot of people I 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 was following ended up there. Then, Twitter started getting progressively worse under the new management. Elon’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. Changes, so many changes… and things changing meant that I 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: As usual, I 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 had a ton of fun doing it. So in this blog post, I wanted to share some of the things I’ve been working on lately. The Dead Bird Site 🦤 I had a bunch of private tools written for the Twitter API. For example, I had a script that downloaded all tweets from my timeline and some lists to a local database. I 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. And then Elon shut off access to the API (unless you can afford $100 per month for a “hobbyist” plan), which meant I had to try to find other ways to get that data. I quickly got the idea that I could somehow intercept the JSON responses that the Twitter webapp (I 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 need. The problem is how to get them; I wanted to get data from my personal timelines, so I couldn’t do anonymous requests, and I didn’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. So the approach I settled on was to passively collect the requests in the browser, using Safari’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 can tell, there is no way for request monitoring extensions to look at the content of responses, unless you inject scripts to …
RubyFrontend
Social media update - Elon's Twitter and Mastodon
Dec 22, 2022
Update 01.03.2023: Updated Mastodon address - my previous instance has been unexpectedly shut down and I had to make a new account. I’ve decided to set up my own server to make sure it won’t happen again. Update 09.11.2023: I made a follow-up post which talks about the social media related projects I’ve been working on this year, and about Bluesky, where I’m spending most of the time now. 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’ve surely noticed if you log in to these even occassionally. Twitter has been my internet home for over 13 years now. I started using it when my colleagues from Lunar Logic showed it to me, and especially in the recent years it’s been my main source of information and news. It’s where I went to keep track of what was happening in the Apple/Swift world, find useful tips about UIKit, SwiftUI 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 Covid pandemic and the Russian invasion of Ukraine this year. Like most of the people I’m following, I’m not very happy about Elon’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’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 wish this all hadn’t happened, and I’m angry at the people who made it happen for their own gain. 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’m not ready to give up on this site as long as the feed is loading and there are some tweets left to read. I’ve been trying out Mastodon like everyone else and I 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’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 had some ways of using Twitter that worked for me and a bunch of private tools I wrote for myself to help me automate some things - I will have to figure this all out again now. So I am on Mastodon, if only because I don’t want to miss out on things, but I am still on Twitter and I’m planning to stay and keep posting there, as long as it stays usable. I will probably be posting more on Twitter than Mastodon, because I feel more comfortable there. I hope some of my friends and people I follow stay on the platform, or at least check in from time to time. So here’s where you can find and follow me (updated 09.11.2023): 🦋 Bluesky: @mackuba.eu 🦣 Mastodon: mackuba@martianbase.net Twitter: @kuba_s…
Social
New edition of the "Guide to NSButton styles"
Dec 30, 2021
Note (Oct 2023): The names of the buttons have been changed again in the SDK in macOS Sonoma - I will update the blog post again once I have Sonoma on one of my Macs :) Back in October 2014 I wrote a post about different styles of NSButtons. That was in the era of OS X Yosemite and Xcode 6. I started researching what each kind of button available in Interface Builder was for, because I couldn’t figure that out from Xcode and the built-in documentation - I dug a bit into the Human Interface Guidelines, some older documentation archives and into Apple apps themselves. I collected everything into a long post that went through all the button styles and described what I could find about each one. It seems that a lot of people also had the same problem, because the post turned out to be extremely popular. It’s around #3 in total page views on this blog, and 7 years and 7 major macOS versions later it still usually comes out #2 in monthly or yearly stats and still gets a couple hundred visits a month. Even with greatly improved documentation in Xcode and much expanded content in the modern HIG, there’s clearly demand for this kind of information collected in one place. However, the post was kind of asking to be updated for a long time now… The original screenshots were made in 1x quality, since I didn’t get a Mac with a Retina screen until the end of 2016. Big Sur was released in the summer of 2020, significantly changing the design of the OS, and making Catalina suddenly look outdated (to the point that I’ve seen some people already call the Yosemite-Catalina era design “classic macOS”!). Some new button variants were added, some older buttons were no longer used in system apps the way I presented them, and the button styles available in Xcode were no longer shown and described as shown on screenshots from Xcode 6. The Big Sur launch seemed like a great moment to give that post a refresh, and I started working on it at the end of last year, but then 2021 came and this year turned out to be kind of rough - surprisingly more so than 2020… as it was for a lot of people, I suppose. I only managed to get back to this project this month. I thought it would take maybe a week or two… it took around three in total 😬 That included setting up new Mavericks and Yosemite installations in VirtualBox to get updated Retina screenshots from there, building a number of versions of a sample app full of all kinds of buttons that I took a ton of screenshots of on several macOS versions, cutting every screenshot pixel-perfect to size a few times, merging different versions of the same information from a few different sources, including versions of HIG going as far back as 2006, looking through Apple apps searching for buttons, and view-debugging some of them with SIP turned off to check what controls were used there… whew 😅 I’m really happy with the result though. This is now by far the longest post on the blog, with around 11k words total (although around 1/3 of tha…
CocoaMac
TypeScript on Corona Charts
Oct 15, 2020
Back in spring I built a website that lets you browse charts of coronavirus cases for each country separately, or to compare any chosen countries or regions together on one chart. I spent about a month of time working on it, but I mostly stopped around early May, since I ran out of feature ideas and the pandemic situation was getting better (at least in Europe). The traffic that was huge at the beginning (over 10k visits daily at first) gradually fell to something around 1-1.5k over a few months, and I was only checking the page myself now and then. So it seemed like it wouldn’t be needed for much longer… “Oh, my sweet summer child”, I kinda want to tell the June me 😬 So now that autumn is here and winter is coming, I suddenly found new motivation to work on the charts site again. But instead of adding a bunch of new features right away, I figured that maybe some refactoring would make sense first. I initially built this page as a sort of hackathon-style prototype (“let’s see if I can build this in a day”), but it grew much more complex since then, to reach around 2k lines of plain JavaScript - all in one file and on one level. I started thinking about how I can make this easier to manage, and somehow I got the idea to try TypeScript. Why add static typing? I used to believe that static typing was just unnecessary complication. My first programming experiments back in school were in Pascal and very bad C++. At the university, pretty much everything was either plain C or Java (or C#, depending on which group you picked). It was only near the end of the studies that I suddenly discovered Python and later Ruby, and it was like a breath of fresh air. I also read Paul Graham’s book “Hackers and Painters”, which made a big impression on me, steered me away from big corpos towards the startup world (for which I’m forever grateful), and also showed me how much better dynamically typed languages (specifically Lisp) were than statically typed ones. I spent the next few years writing mostly Ruby and some JavaScript for the frontend, and I loved it. I still love Ruby and to this day I use it for all my scripting and server-side code. However, at some point I also started building Mac and iOS apps, first in ObjC, and then in Swift. ObjC just felt like so much unnecessary boilerplate, and it really was. Then Swift came and simplified everything, but it exchanged the boilerplate for a very strict type system, much stricter than anything I’ve seen before. It was annoying to have to explain the compiler which property can be nil and when and what to do with it, or what to do if this JSON array does not contain what I think it does. But after using Swift for a few years, I really appreciate the feeling of safety it gives you. You have to put in more work up front, but once you do, and once it compiles, you can be sure that whole categories of possible errors have already been eliminated before you even run the app. You have to do fewer build - run - find …
JavaScriptFrontend