Skip to content
Kuba Suder šŸ‡µšŸ‡±šŸ‡ŗšŸ‡¦
Author

Kuba Suder šŸ‡µšŸ‡±šŸ‡ŗšŸ‡¦

@mackuba.eu

Indie Mac/iOS & web developer • Currently building random fun things on ATProto using Ruby and occasionally JavaScript → https://blue.mackuba.eu "mostly-happy-but-occasionally-grumpy Ruby guy" - Why šŸŒ Kraków, Poland šŸ‡µšŸ‡±šŸ‡ŖšŸ‡ŗ • āœļø

MacKuba blog
MacKuba blog
Jul 19, 2026
ATProto in Practice #1: Identity
I’m starting a new blog post series that I’ve named ā€œATProto in Practiceā€. IĀ want to go through some practical tasks that you’re likely to need when working with the protocol, using Ruby and my Ruby gems for the examples. These will (hopefully) be a bit shorter than my standard book-length blog posts here 🫠 If you’re new to the AT Protocol and you haven’t read my long Introduction to AT Protocol post that goes through the whole architecture and defines the various pieces of it, IĀ recommend you read that one first, because IĀ won’t be explaining everything from scratch again here. Let’s start with something pretty fundamental: user identity. DIDs & handles Every account has an immutable identifier called a DID (Decentralized Identifier), e.g. did:plc:z72i7hdynmk6r22z27h6tvur. This ID serves a similar role as an UUID might in an SQL database – it’s used everywhere behind the scenes in the protocol for any references between accounts and records, in URIs and so on. But generally every account also has a human-readable handle assigned to it, which can be changed at any time, and this handle is normally what you see in the UI, often in user-facing URLs, what you use to log in with, etc. The handle is a domain name, e.g. @python.org, and any existing domain name you own and use for your website can be used as your handle. As you start building on ATProto, one of the first things you might need to do is convert between these two identifiers. For example, a user logs in to your app using a handle and you need to first ā€œresolveā€ it to a DID, or a URIĀ or other reference in a record points to a DID, but you want to show the corresponding user’s handle in the UI, and the handle is not included in the data you got. A useful UIĀ tool for quickly looking this up is internect.info. If you look up e.g. ā€œfirefox.comā€ there, you will get a page showing details of Firefox’s ATProto account: the DID (did:plc:m424cqoxwhxgjutbta7jmrur), when it was created, assigned PDS hostname, and so on. Now, how to do the same thing in code? Every DID has a so called DID Document, a JSON file storing some basic info about that identity, and among other things, that document includes the alsoKnownAs field, which lists the assigned handles, in the form of at:// URIs with only the first segment. Almost every account will have exactly one handle assigned, but it might happen that one will have zero or more than one. It might also happen that the array will contain some invalid strings which aren’t proper handles – e.g. don’t include the at:// prefix, or only contain one word and no periods. You need to filter only the valid ones. The assignment is bi-directional – a DID has one (usually) handle assigned in its document, and a handle resolves to a DID. You should ideally check the assignment in both directions, because it could happen that the other sign of the assignment is no longer valid, or worse – has never been real. That is, when you resolve a handle to a DID, check if th…
SocialRuby/Rails
MacKuba blog
MacKuba blog
Jul 13, 2026
Social comments on my blog
IĀ had a comments section on this blog since the beginning. IĀ always felt like a comments section was kind of a requirement for a blog: it’s not just a website where you publish posts, it’s a website where you publish posts and other people can comment on them. IĀ strongly disagreed with bloggers who openly declared that they’re removing comments from their blog because their blog is not a place for others to add their remarks, and that they can always write to the author on Twitter if they want to contact them. But one obvious problem with comments on a blog, which got worse and worse over time, was spam. People tried to solve it e.g. with captchas, though as the bots got smarter, captchas had to get more complicated and annoying. For a very long time, IĀ had a super simple captcha in the comment form that just asked: ā€œAre you a human? (yes/no)ā€. You just had to write ā€œyesā€ in the text field. And for a very long time it was enough – obviously it’s trivial to make a bot add an additional query parameter, the point was that nobody would think about it when writing generic spambots, and my blog isn’t famous so the bot authors wouldn’t have come across it. Things got worse around 2018-19. IĀ started getting comments (IĀ had email notifications for them) that were spam that obviously got around the captcha. So IĀ started tweaking the captcha, adding multiple slightly more complex questions, but still such that you could answer them if you’re a developer or an Apple user – ā€œWhat’s the name of the Mac operating systemā€, ā€œWho is currently the CEO of Appleā€, and so on. But it only worked for some time, and then IĀ started getting spam again, while making the questions progressively harder over time: At some point IĀ figured that this had to be actual people, rather than bots, adding the comments (talking in general, not about Jerry here above), people sitting at a computer, browsing websites and filling the forms by hand, because there was no way a bot would figure out that the question was a captcha and that it had to google for a name of a function (note, this was in pre-AIĀ times). The comments were usually either in Russian or Ukrainian, or linking to some local service companies in some random small town in the US. There was no way IĀ could out-captcha an actual human that can google for an answer. But it wasn’t a huge problem, IĀ didn’t get many of those comments, and it was fairly easy to filter them out still by content, so IĀ didn’t bother changing the system further. But here’s the thing: it’s not 2010 or 2018 anymore, and some other things have changed in the meantime too. IĀ think it’s a bit of a truism that blogs and ā€œblogosphereā€ aren’t what they used to be 15+ years ago. People post their thoughts on social media, Instagrams, Facebooks, LinkedIns, Substacks, record Tiktoks, YouTube videos or podcasts. Reading and writing blogs and commenting on them is a very niche hobby, let’s face it. After 2020-21, when for a while IĀ was getting a ton of comme…
SocialJavaScript
MacKuba blog
MacKuba blog
Feb 4, 2026
Running Bluesky PDS undockered
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