RSSAmplifier

Cory Dransfeldt • Posts · Apr 8, 2026

Personal site infrastructure, diagrammed

0
Sign in to vote or save

Cory Dransfeldt · Cory Dransfeldt

That I even need and have made a diagram of the infrastructure for this site speaks to how over-engineered it is. Yet, while it is ostensibly a personal site, it's a personal site that's replaced many services that I previously used (last.fm, Letterboxd, Trakt, oku, The Storygraph, MusicHarbor et al). I maintain a lot of this for the same reasons anyone might use the services I replaced.

It's fair to say that building a blog in Laravel would, on its own, be overkill. But Laravel allows me to support both the blog portion and everything else that I've built here. Everything built and integrated here originated on Coolify before being migrated off. One of the many benefits of that migration was that I was able to move everything but the public-facing Laravel application behind Tailscale.

The structure of this is illustrated in the diagram, below, for which this post is named.

The public facing (coryd.dev) server runs nginx, Swoole/Octane and redis with the Laravel application code deployed to it. Filament is deployed here, but is only accessible from my Tailnet IP.

Data for the frontend of this site is fetched from postgREST, which routes through pgcat to the postgres database where everything is stored. Filament interacts directly with the database via pgcat. All of this communication occurs over the Tailnet and is not public.

Each server (and the NAS) diagrammed above allow ssh only via Tailscale. Applications with web interfaces that are accessed with any regularity have records in a CoreDNS instance that point to the appropriate Tailscale IP address. UFW is used to restrict access to the apps, Audiobookshelf and Navidrome to the Tailnet only. Jellyfin sits on a NAS on my home network accessible, again, only on the Tailnet.

The Filament admin is where all of the data and content on this site is managed. Status updates, posts, links, authors, pages, concerts and snippets are managed manually. Everything else is updated automatically, with the occasional manual intervention.

My GoatCounter instance sits on my apps server. Rather than redeploying it or only making specific portions public when putting it on the Tailnet, I've been sending access logs to it via its API and filter out 403s before sending them. The GoatCounter UI can become a bit noisy when ingesting logs to measure traffic, so I've created an analytics view in Filament. This view has all of the same data as GoatCounter, but separates and deduplicates feed and file requests into their own tabs. Any pages with titles stored in Filament (posts, books, movies, shows, artists and on and on) have their titles displayed by using the request URL to find said title. I've also added the ability to block paths, prefixes, segments and extensions and have a curated list of domains that I exclude from referrers and disallow in contact form submissions.

New deployments for this site are triggered manually from the Filament dashboard. I click a button, a post is sent to a webhook on the apps server which then runs a shell script to clone the latest source from Forgejo, build it and copy it to the site (if you ever see brief downtime, that's typically the cause).

I read books in a custom PWA I built that leverages my Audiobookshelf instance's authentication and library to allow me to read the ePubs I store alongside audiobooks and podcasts. It stores its own progress in SQLite (more on this in a future post — there's a good deal of logic in place to maintain progress reliably across screen size and devices) and updates the progress on Audiobookshelf. It only reads progress from Audiobookshelf if none is stored. My reading progress, represented as a percentage, is fetched from audiobookshelf every 15 minutes. When progress is first updated for the day, my reading streak is incremented.

Every episode of a show or movie I watch via Jellyfin is logged by having Jellyfin's webhook plugin post to a Laravel endpoint that handles matching the payload to the appropriate show or movie. If there's no movie or show to match, it will import the matching media from TMDB and attach the first poster and backdrop images available. For shows where I'm tracking upcoming episodes, it will mark the scheduled episode record as watched and log the play.

I listen to all of my music via Navidrome. I wrote a scrobble plugin that posts each listen to a Laravel endpoint that associates it with the appropriate track, album and artist. If a track, album or artist are not found, it sends me an email with the details using forwardemail.net's API.

When I add a concert in Filament, it's associated with an artist (should one exist). When adding a venue to associate with concerts in Filament, coordinates are fetched from OpenStreetMaps by querying their coordinates API. These coordinates are used to link to the venue location on OpenStreetMaps on the frontend.

Additionally, there are a pair of nightly jobs that leverage the MusicBrainz API. One to add the mbid MusicBrainz uses to identify artists to each of my artists, which is done by searching using a combination of their name, location and genre. I go through periodically and confirm the IDs that have been added. The second uses the confirmed artist IDs to search for releases for each artist and adds a pending release record in Filament. If I want to track the release, I'll import it as a proper album record associated with the artist. If not, I'll dismiss the record.

To quickly add content to my site, I've built several import tools accessible from my Filament dashboard. The first allows me to import books. This defaults to using my Audiobookshelf instance's API (I should probably remove the others) while also supporting Google Books and OpenLibrary. I pass the appropriate ID or ISBN and the importer populates the book record.

My movie/TV importer fetches and populates data from the TMDB API based on the supplied TMDB ID and will also handle downloading and attaching poster and backdrop images from supplied URLs. I have an additional TV-specific importer that will attach unwatched episodes of a show associated with the supplied TMDB ID. This allows me to keep track of my progress — it's also done automatically for shows that have the ongoing bool set to true.

Finally, I have an artist importer that fetches artist and album data from my Navidrome instance. For new artists it will create the artist record, attach albums, attach tracks to albums and download and associate art. If I reimport an artist it will add only new albums to the artist. In the event I have a duplicate album or artist added, there is logic built into each model to handle combining the two.

Once I've imported something, I'll give it a quick manual review and make any necessary updates. Artists are typically the most involved and require that I update the slug, associate the appropriate country the artist is based in and author or update a bio. I've authored a Navidrome agent plugin that will periodically query a Laravel API, retrieve updated artist bios and sync them back to the Navidrome page for an artist.

The other connections of note are bunny.net and Mastodon. The former is where all images are uploaded. The latter is my favorite social "platform" (protocol?) — I've added support to syndicate posts, links, books, movies, concerts and status updates from Filament to Mastodon by enabling a toggle and saving the item. Once syndicated, the syndication time is noted and the toggle is disabled.


That covers it (for now). Everything I can track and share automatically, I do. Everything I can't, I support sharing manually.

Read the original on coryd.dev

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.