Genesis
One weekend, I was lying in bed with my home projector on, deciding to finally watch a movie I'd kept on the back burner for a month: The Bourne Supremacy. The movie was good, though I think I liked the first one better.
But as I watched, my twisted developer mind couldn't just appreciate young Matt Damon doing spy things. I started thinking about how awesome it would be to add a section to my personal website where people could see the movies I'd watched and read my opinions on them. You know, as one does.
Initially, it seemed straightforward. Just create a new movie-corner directory, add a page.tsx, dump a model and some data into a lib.ts file, and I'd be good to go.
But then I thought a bit more, and a new idea came to my head. Instead of a static, manual, hard-coded list of movies, wouldn't it be much better to have a widget? You know, like those old-school widgets that roamed the internet in the 00s. An iframe that would display my profile from a brand new, dedicated web service handling all this movie stuff.
Let's be honest: I am quite a lazy person. I knew that hard-coding movies, fetching posters, managing metadata myself, and making a git commit every single time I watched a film would quickly become cumbersome, and I'd just stop doing it. Not to mention, adding a new movie from my phone or any device that isn't my laptop would be a massive pain in the ass.
So, the idea for a dedicated web service was born. When the movie finished, I didn't even move from my bed. I just grabbed my laptop, opened it up, created a new directory, and at 10:00 PM on a Sunday, I began what I naively thought would be a simple two-hour adventure.
Now, you might be asking yourself (or rather, asking me) why reinvent the wheel? Services like Filmweb, Letterboxd, IMDb, and probably a dozen others already exist and do exactly what I wanted.
To be honest, if you have to ask that question, you don't know what we as programmers, engineers, and developers actually do. We love to solve already-solved problems from scratch. We are the exact people who look at 14 competing standards, complain that there are too many standards, and proceed to create a 15th one ;)
But jokes aside, I did have a genuine motivation beyond just an itch to write code. I used Filmweb extensively from 2015 to 2020, and eventually, I realized a fatal flaw in these platforms: you don't own your data. Filmweb owns it. Letterboxd owns it. [citation needed, i am not actually sure about letterboxd, so don't sue me.]
Since around 2021, I've become a massive fan of the Fediverse. I absolutely love the concept of the decentralized web. So I thought, hey, this little pet project of mine actually has a highly sensible reason to be decentralized and federated. Instead of trapping my reviews in another walled garden, I could own my database and broadcast my activity to the wider network.
But, wait a second. Fediverse, federation... so many words, so many concepts. What the hell do those even mean? It would be very impolite of me not to explain myself and leave you, dear reader, with this unpinned grenade (is this even an idiom in English??).
So let's take a quick tangent into my obsession with the decentralized internet, shall we?
What the hell is the Fediverse?
If you aren't deep into decentralized tech circles, the Fediverse (Federated Universe) might sound like something out of Star Trek. But you actually already use a federated system every single day: Email.
Think about it. If you have a Gmail account, you can send a message to your boss who uses Outlook, or to your weird uncle who still uses wp or o2 or other old ad-ridden mail provider. You don't all need to be on the same platform to talk to each other, because all of those servers speak the same underlying language (SMTP).
Now, imagine if social media worked like that. Imagine (John Lennon starts playing in the background) if you could use a Twitter account to follow your friend's Instagram page, like a YouTube video, and comment on a Reddit thread, all without ever leaving Twitter.
That is Fediverse. It is a network of thousands of independent social media servers that can all talk to each other.
The magic glue that makes this possible is a web standard called ActivityPub. It's essentially the HTTP of social networking. It is a language, a protocol that social media platforms use to speak to each other. If your server speaks ActivityPub, it can communicate with anyone else on the network, the most famous example being Mastodon. And spoiler alert: Movies Diary entries do show up on Mastodon feeds!
Or so that is the theory.
Personal career growth limbo
Okay, so I hope that clears up what the Fediverse is and how federation works. I promise I will come back to the ActivityPub stuff later.
But building a federated node wasn't actually my first, or even second, motivation. You see, I have been programming since I was 11. Now I am at the ripe age of 24, and I love programming as much as I did as a kid. Maybe even more so, now that I'd consider myself a fairly decent software engineer.
But I would love to become a software architect one day. Right now, I'm in this kind of professional limbo where I am utterly obsessed with architecture, system design, and all of that high-level jazz. Low-level details just aren't as important to me anymore.
So, before I even wrote a single line of code, I decided on the core principles of this project. The idea itself was simple (it was essentially just a CRUD app) but that doesn't mean you have to do it the half-assed way.
Recently (well, five months ago), I bought a few books about, surprise surprise, software architecture and Domain-Driven Design (DDD). Naturally, I haven't read them yet. But I did read a few articles! I am in no way knowledgeable enough to give lectures on DDD, but I decided this project would strictly follow my favorite pattern: Hexagonal Architecture, splitting the core business logic cleanly into Domain and Application layers.
Why go through all this trouble? It was supposed to just be a widget, right?
Well, yes. But we need a way to store data, which means we have to bring in some persistence. That naturally means bringing in a database engine. And that makes me think: hmmm, what if I want to swap out the database engine later? It's nice to abstract it from the start so it won't be a pain in the ass to change down the road.
Then there's the metadata. We need a way to fetch movie details from free APIs. That's another thing we should abstract. There are at least two providers I know of (TMDb and OMDb), and our core domain shouldn't care which one we actually use.
And then we have the movie posters. We need to store image files. We could have multiple storage backends. I run a MinIO instance on my home server, but someone else might prefer to use their local file system, or maybe even dump blobs into a PostgreSQL table.
As you can see, this simple little pet project surprisingly has a ton of dependencies and underlying complexity. And that's not even mentioning the fact that we definitely need a background worker, which brings in a whole new set of ports and adapters to implement.
So, that explains the Hexagonal Architecture. But I had a few other guiding principles.
First, I absolutely didn't want to use React, or any JavaScript, for that matter. In the spirit of the old-school retro widget, I figured classic server-rendered HTML templates with just a pinch of CSS would do just fine. At the same time, I didn't want to lock users into just my web client. I wanted to expose a clean REST API so anyone could build their own custom clients if they felt like it.
And the final principle: I love open-source, and I am a huge advocate for self-hosting. I want my projects to be incredibly easy to spin up on a home server, and just as easy to maintain and extend.
This is the ultimate justification for using Hexagonal Architecture on a "pet project." If the codebase ever grows, or if I want to swap out pieces in the future, working with it won't feel like untangling a bowl of spaghetti.
Sunday, 10 PM. Let's go.
Alright, enough philosophy. Let me tell you what actually happened.
The language was never a question. I write everything in Rust. Personal projects, work stuff, throwaway scripts that any sane person would write in Python. Rust. It's just how my brain works at this point. I could go on another tangent why I despise Python and other languages, but I will have some mercy for you, dear reader, and not bore you with that.
So I opened my laptop, created a new directory, and did what I always do first: I wrote the domain layer. Not a web server. Not a database schema. Not even a "hello world" endpoint. The domain. Movie. Review. Rating. Trait definitions for MovieRepository, MetadataProvider, PosterStorage. Pure types, zero dependencies, zero I/O.
Then the application layer. The use cases. LogReview. GetDiary. FetchMovieMetadata. Just orchestration through the ports. At this point, the project had no idea what a database was. It didn't know HTTP existed. It was pure business logic floating in the void, and it compiled, and it was correct.
I always start here because the domain and application layers are the soul of your software. They're the most stable part. They only change when business rules change, which, for a movie diary, is basically never. Everything else (database, web framework, storage) is just details. Adapters. If you get one wrong, you just unplug it and write another. You don't have to be nearly as careful with them.
Now, most languages let you enforce these boundaries: separate projects, separate assemblies, whatever your ecosystem calls them. The tools are there. But not everyone uses them. It's very tempting to just throw everything into one project and say "eh, I'll keep the layers separate by convention." And you will, for about two weeks, until it's 3 AM and you just need to quickly access the database from the domain layer, just this once, just a small thing, let me just add it here and be done.
I know myself. I know my lazy human brain would absolutely do that.
So I used the strongest guard I had: separate crates. Each layer, each adapter, its own crate. The domain crate cannot import the SQLite crate. The compiler literally won't let it. Every port is a trait. Every adapter is a crate that implements that trait. No willpower required.
And it's not just about me. Maybe I'd stay disciplined. But if someone else ever contributes to this project, they don't have to know the rules. The compiler already knows them. Of course, in the end the ball is always in the programmer's hand. You can always work around anything if you really want to. But at least you have to try to break it, instead of breaking it by accident.
Once I was happy with the soul of the app, I started plugging things in. SQLite for persistence. OMDb for movie metadata. Askama for HTML templates. Axum for routing. Each one its own crate, implementing the traits from the domain. By somewhere around midnight, maybe 1 AM (time gets fuzzy), I had a working movie diary. Register, log in, search for a movie, write a review, see your diary. Posters and everything.
A sane person would have stopped there. That was the widget. That was the whole original idea. Ship it, embed it, done.
But then I thought: okay, but what about federation?
Down the ActivityPub rabbit hole
Remember the whole Fediverse tangent from earlier? Yeah, that came back to haunt me about four hours into the project.
I had a working CRUD app. Everything worked locally. And in that post-midnight, slightly delirious state of flow, my brain decided this was the perfect time to federate the thing.
I'm not insane enough to implement ActivityPub from scratch, though. There's a great Rust crate called activitypub_federation that handles the hard stuff: HTTP signatures, JSON-LD, inbox/outbox routing, WebFinger, all of that. My job was to wrap it and wire it into my project.
I ended up creating two crates, and this split wasn't random. Movies Diary wasn't the only project I had in mind. I was already planning other apps that would need federation, and I knew that if I mashed the protocol glue directly into this project, I'd be copy-pasting hundreds of lines next time around. So I split it: a generic activitypub_base crate that wrapped activitypub_federation with all the Axum boilerplate, and a Movies Diary-specific activitypub adapter that knew about my domain.
That bet paid off fast. activitypub_base later became its own standalone library, k-ap, and I reused it in Thoughts, my social platform project that I've been building on and off since 2024 (if you've read my previous posts, you know the one). ActivityPub integration was always on the roadmap for Thoughts, and thanks to k-ap, it was basically plug-and-play. HTTP signatures, actor resolution, inbox routing, all already done. I just wrote the adapter for Thoughts' domain types and federation worked out of the box.
Now, adding federation wasn't free. I did have to go back into the domain and application layers. New ports for tracking remote actors and followers, new events like ReviewPublished and FollowReceived, new use cases for incoming federation activities. The domain grew. But it grew cleanly. The existing code didn't break, nothing got tangled. The movie logging flow didn't suddenly need to know about ActivityPub. New federation ports just sat alongside the existing ones, and the adapters plugged in the same way as everything else.
By Monday morning, it worked. I could follow a Movies Diary user from Mastodon. Reviews showed up in my feed with proper hashtags. Other Movies Diary instances could federate with each other. Or, well, it worked well enough. The next two weeks were spent on, let's say, "spec compliance refinements." ActivityPub is a fun specification to work with.
One thing worth mentioning though. Movies Diary is not a typical Fediverse citizen. Most federated apps are general-purpose. Mastodon handles any kind of post, Lemmy handles any kind of discussion. Movies Diary is opinionated. It broadcasts to everyone: follow a Movies Diary user from Mastodon and you'll see their reviews in your feed, no problem. But incoming? If you try to send a random Mastodon post to a Movies Diary instance, it'll just quietly ignore it. It only accepts activities that are compatible with its domain: reviews, watchlist entries, stuff that actually makes sense for a movie diary.
That's deliberate. Movies Diary doesn't pretend to be a social network. It's a specialized node that speaks the common language just enough to participate, but stays true to what it is.
So, what does it actually look like?
Enough talking. Let me just show you the thing.
User profile with diary entries, stats, and reviews
The global feed showing reviews from all users
Movie detail page with cast, crew, ratings, and reviews
The watchlist — add movies you want to watch later
A review showing up in a Mastodon feed via ActivityPub
The same review appearing in Thoughts, my other federated app
The widget embedded on my personal site's Movie Corner page
Was it worth it?
Since I started this project, I've watched three movies and logged every one through Movies Diary. Added three films to the watchlist. Reviews federate to Mastodon, posters get fetched automatically. I don't have to open my laptop, make a git commit, or touch a JSON file. Phone, few sentences, submit. Done. I love it.
Hexagonal architecture and DDD on a "pet project"? I stand by it. It's just the best way to write software. It makes sense from day one, it scales without pain, and it keeps things maintainable even when you keep bolting on features at 2 AM. I will keep building software this way for the foreseeable future.
And yes, I looked at Letterboxd, Filmweb, IMDb, complained about the state of things, and went ahead and built yet another one. I am part of the problem. But this one is mine. My data, my server, my rules. It federates, it's open-source, and it started with Matt Damon running from the CIA on a Sunday night.
I wouldn't have it any other way.
Check it out
If you want to see the thing in action: movies.gabrielkaszewski.dev




