// Personal website of Chris Smith

Building a family of single-user services

Published on

Last month I made and wrote about a personal service to track my workouts and record personal bests. Since then, things have escalated. I now have eight different services running in the same “family”:

Name Purpose
Collegiate Room Simple weather overview for my current location
German Mall LLM broker for use by other services
Gloss White Weight tracking
Olive Crosses Central service hub
Pompei Band Workout and personal best tracking
Speed Carp Spaced repetition system
Tiger Island Reading history and library tracking
Typical Horse Location history and analytics

The names are all anagrams of something related to their function. Finding those anagrams brings me an unreasonable amount of joy. Unscrambling them is an exercise left to the reader. On top of those eight, I currently have four other service ideas sat in my todo list. It’s going to be a big family.

A postmortem on the previous hodge-podge

Most of this functionality isn’t new to me. When I started out playing around with LLMs, I created a “personal data aggregator” that just kept getting things bolted on. I said at the time:

My biggest just-for-me project is a web app that I started to help me aggregate film recommendations. It’s since morphed into a general personal data aggregation service: it deals with data from GitHub, Todoist, Letterboxd, TMDB, Healthkit, and others. It also lets me make re-orderable lists, store recipes, and more. Parts of this could definitely be open sourced, and I might carve them out at some point, but it’s mostly a glorious hodge-podge of things specific to me. Having all these services in one place lets me make quick and dirty automations, for example: when I create a Todoist note on my phone or watch, I often forget to set the due date, so it doesn’t show up in the “Today” view. It was literally a few lines of code to plumb things together so any inbox task without a due date gets set to today automatically.

In a twist that nobody could see coming, it turns out that a “glorious hodge-podge” is actually quite hard to maintain and very annoying to work with. It didn’t help that it had a fairly extensive Svelte frontend and I really am not a fan of JavaScript. I ended up using it less and less just because it wasn’t fun to use or work on, and I didn’t want to waste the limited LLM tokens on doing massive refactors or reworks.

I’ve got almost two decades of professional software development experience under my belt, so you’d think I would have learnt the lessons on how to write maintainable software. Hell, my CV even mentions that as something I’m particularly good at. So what went wrong?

There were a few factors. One of the obvious things to blame is that it was written mostly with LLMs. Most of that blame isn’t very honest, though. In the year or so since, LLMs have definitely got better at writing good code; but I was manually reviewing all the code at the time, and telling it quite carefully what to implement and how to do it. So the buck really stops with me.

I think it’s actually more because it was my first larger project using LLMs. It’s a bit like learning a new programming language. I don’t think the first sizable project I’ve written in any language has ever been any good. You can read and learn as much as you want, but you need to actually run into some problems headfirst to get a grip on how the language works. Or at least I do. LLM coding agents are basically the same deal.

What the LLMs actually did was mask several problems. The architecture wasn’t right for how the service developed. It started out as a single-purpose app and gradually got things bolted on, without any major changes. Everything ended up in horizontal slices: a massive database layer with every query used by every part of the app, a massive HTTP layer, and so on. Working on a single feature became a tap dance all over the source tree. This sort of friction is normally quite apparent when you’re writing code by hand, and at some point it hurts enough that you throw in the towel and do some refactoring. But the LLM will happily plod on without exposing that pain, so the project needed a more active kind of architecture management that it just didn’t get.

The other problem they covered up was how brittle and unmaintainable the JavaScript frontend was. I’m not even sure how I ended up with a Svelte frontend in hindsight. It’s my preferred framework, but I still only reach for it as a last resort when there’s no other way to avoid using one. It seems like there were lots of ways to avoid it here, but I seem to have… well… avoided them. Anyway, each feature got its own slew of Svelte components, without much thought to reusability, shared patterns, or anything sensible. Some of them were just thin wrappers around data supplied by Go, others had vast swathes of logic in them, with no real rhyme or reason as to which way that would fall. It’s the same issue as before: a human would look at it, say “WTF?” and suggest tidying it up; instead, the LLM just gets on with what you tell it to do.

There’s also a big non-LLM factor: one of the primary reasons all of this unrelated functionality got tossed into the same app was that I’d previously worked on a few projects with a microservice architecture and really didn’t like it. My conclusion was they only really worked in large environments where you can dedicate people or teams to working on all the boilerplate that happens from maintaining lots of interconnected projects. Dumping everything and the kitchen sink into one badly architected monolith was, perhaps, not the best rebound from that.

Pompei Band and beyond

When I wanted to implement Strava at home my PB tracking app, it seemed fairly natural to build it out as a new service, rather than bolt it on to the slowly decomposing hodge-podge that was giving off a funny smell. Then when I wanted a weather dashboard, why not do it in the same style as Pompei Band? The same serving architecture, the same page layout… I could even come up with a similar weird two-word name!

Then wouldn’t it be nice if the weather dashboard knew where I was, so it could show the local weather? I could just build that functionality in, but it’d also be a good opportunity to carve out the location history feature from my old service and make a new, shinier one. Then basically every time I even thought about going near the old service, I mentally sketched out a replacement service.

A screenshot of Olive Crosses, showing links to other services with icons based on their two-word names

Olive Crosses showing links to the other services

At one point I had the thought that it’d be nice if the services could all link to one another, but I didn’t want to have to touch every service whenever I added a new one. So I made a “hub” service, which maintains the One True Catalogue, and provides a bit of JavaScript the others can all embed to add a navigation sidebar.

So after complaining about microservices and JavaScript, it sounds like I have landed on microservices (or at least miniservices) and JavaScript? Sort of yes, but sort of no. Each service is still its own thing and makes sense independent of the others. There are some cross-service dependencies, but they’re very simple and don’t have the same cognitive or boilerplate overhead as a login service that talks to an authentication service that queries a user manager and so on. And as for the JavaScript, it’s almost exclusively used for progressive enhancement and a few limited client-side visual things like maps and graphs. All of the logic lives in Go, which is a lot easier to maintain and test, and all of the pages are simple Go templates.

Having these services built in a way that I actually like has motivated me to play around a lot more with new features:

  • My workout tracking app pulls photos from my private Immich server that were taken during workouts, and shows them alongside. Now I can see all my obligatory end-of-run selfies in one place.
  • The location history app can detect trips away from home, and get an LLM to automatically title them. My most recent, very exotic, trip has the title “A fortnight in Milton Keynes and Coventry”. Each trip gets its own little map, and in the future I want to pull in images like with workouts.
  • My weight tracking app now exports weigh-ins to Garmin, as it doesn’t fully integrate with Apple Health (it will export its own weights, but not import others, like the ones from my Withings scales). Stupid walled gardens, but thankfully I can fix it myself.
  • My book tracking app automatically cleans up the titles of books from the nonsense provided by Amazon to something more palatable. “Killing Eve: Long Shot: A twist on obsession, betrayal and revenge from Luke Jennings, author of the TV sensation Killing Eve” becomes “Long Shot (Killing Eve #5)”. It does this by passing the title to an LLM with a bunch of rules.
  • The hub generates me a snippet of Tailscale configuration that defines the minimal grants needed to allow the intra-service dependencies to work.

The LLM integrations were inspired by my friend Shane who recently demonstrated an inventory tracking app he’d made, which used Haiku to process images of boxes and describe the contents. I’ve dabbled with LLM integration before, and always disliked the process of fiddling with the prompts, and debugging when the LLM gave a nonsensical response. So I built German Mall, which makes it a lot easier. It lets me define a “workflow” consisting of a system prompt, initial message, and expected response format. I can browse past runs, tweak the prompt, rerun them to see the change, all without touching the services using them. Quick iteration loops are the best. German Mall also provides markdown documentation for each workflow, so integrating it downstream is simply a case of pointing a coding agent at the right documentation endpoint.

A screenshot of German Mall, showing the workflow configuration for tidying book titles: a form with fields for description, system prompt, user template, output schema, model, max tokens and temperature.

The “tidy book titles” workflow in German Mall

It’s not all fun and games: having a bunch of services all following the same pattern means you need to do cross-cutting maintenance on them now and then. But that’s surprisingly easy with LLMs. I put all the projects in a common folder, and I launch cross-cutting tasks from that folder. Usually I’ll make the change in one project, and then the cross-cutting task is simply “Look at commit XYZ in ./foo, make the same change to the other projects”.

I’ve also ended up with a shared library used by all the projects to cut down on some boilerplate. That’s normally something I’d resist, especially with personal projects, just because the extra overhead of working in a separate project, managing releases of it, and so on, is a bit annoying. But, again, LLMs can do almost all of that grunt work. They effectively reduce the “cost” of maintaining separate services to a negligible level, leaving me to focus on the more fun aspects like coming up with new names.

This content is also published/discussed on these external sites:

See the colophon for information on how personal data is used and retained when submitting this form.

Related posts

A spaghetti carbonara ready meal, fresh out the microwave

An app can be a ready meal

Three years ago I read “an app can be a home-cooked meal” by Robin Sloan. It’s a great article about how Robin cooked up an app for his family to replace a commercial one that died. It’s been stuck in my head ever since. It’s only recently that I’ve actually done anything like Robin described, though. Part of the reason was my brain got too hung up on the family...

Word cloud featuring: game, month, post, blog, llms, obsidian, bunch, notes, playing, site, button, track, project, board, write, social, entertainment, alternative, into, llm-generated, near, forgejo, through

Monthly Meanderings: January 2026

Welcome to the second edition of my monthly meanderings. For a bit of context, you can check out the introduction to the first edition. Website updates It’s been a pretty busy month for chameth.com. Three blog posts: The Meaning of Life — an entry into the IndieWeb carnival where I mostly review a book on Stoicism — Surge Protectors: Marketing vs Reality which is a dump of a rese...

A screenshot of claude-code. I ask it for the permalink for the latest post, and it gives a wrong answer. After prompting it again it gets it right.

Coming around on LLMs

For a long time I’ve been a sceptic of LLMs and how they’re being used and marketed. I tried ChatGPT when it first launched, and was totally underwhelmed. Don’t get me wrong: I find the technology damn impressive, but I just couldn’t see any use for it. Recently I’ve seen more and more comments along the lines of “people who criticise LLMs haven’t used the...