Why#
There is a point in every infrastructure project where the foundation stops being interesting and the services start to matter. The database is running. The reverse proxy is configured. Authentication works. And then the question becomes: what is this actually for?
For HomeStack the answer is specific. Files the family can access without Google. Photos that do not live on someone else’s server. A financial overview that belongs to us — not to a bank’s export format or a SaaS subscription we pay for indefinitely.
This phase was about building those three things. Each one represents a category of data that most families have quietly surrendered to Big Tech without a clear moment of decision. Getting them back required making explicit choices about where data lives, who can access it, and what happens to it when a service stops existing.
Family#
Immich was the one that landed. A photo platform with a mobile app that looks and feels like Google Photos — the same infinite scroll, the same face detection, the same “on this day” memories — but running on a server in the home office. I uploaded ten test photos during setup. My partner walked past while I was doing it and asked what I was looking at. I showed her. The reaction was different from anything before. Not “what is it” but recognition. This is what we use every day. This could replace that.
Seafile was quieter. A file sync client that works. Documents, folders, shared libraries. The family will use it without knowing much about what it is — which is exactly the goal. Complexity belongs to the builder, not the user.
Metabase is something my partner will probably never open unprompted. A financial dashboard, linked to a custom database, showing spending by category and net worth over time. It exists because I wanted to understand where our money goes without exporting CSV files from a bank portal every month. Whether it becomes a household tool depends on whether the data in it becomes trustworthy over time. That takes months, not a week.
Tech#
Each service taught something different.
Seafile required a second database engine. The unified PostgreSQL strategy had one documented exception already — Uptime Kuma using SQLite. Seafile CE requires a MySQL-compatible database and supports nothing else. MariaDB became the second exception. The pattern held: name it, justify it, document it, move on. What matters is that the backup strategy accounts for it — a mysqldump hook alongside the existing pg_dump hooks, tested before the service was considered done.
Immich required its own PostgreSQL instance with vector extensions for machine learning embeddings. The decision to give it a separate container rather than adding extensions to the shared database was straightforward — vector workloads are categorically different from relational queries, and the risk of destabilising the shared instance was not worth the operational simplicity. The harder problem was TLS.
Caddy’s tls internal does not use the OpenSSL Root CA generated in an earlier phase. It generates its own certificate authority — a short-lived intermediate hierarchy that no other service knows about. When Immich tried to reach Authentik over HTTPS for the OIDC discovery endpoint, the TLS handshake failed. The fix required extracting Caddy’s CA chain via its admin API, combining it with the system certificate store, and mounting the result into the Immich container alongside a Node.js-specific environment variable. Node.js 24 does not read from the system certificate store automatically — it uses a compiled-in bundle. This is not documented prominently. It is the kind of thing you find out by reading error logs carefully.
The financial stack started with a rejected tool. Firefly III is well-maintained, widely used, and failed one test: it is not a native OIDC client. Integrating it with Authentik would require a middleware proxy container — one extra failure point, one more thing to maintain, and a workaround rather than a proper integration. Every public-facing service in this stack needs to be a native OIDC client. That is a principle, not a preference. Firefly was evaluated in the Lab, found wanting on that single criterion, and discarded.
What replaced it was a custom PostgreSQL schema with Metabase as the visualisation layer. The data model started as an MVP with eleven entities and grew, through two rounds of redesign, to fifty. Polymorphic ownership — a single owner entity that can represent a person, a partnership, a household, or a business activity. Validity periods on every record, so historical data can be reconstructed. A symmetry principle discovered during Lab testing: assets, liabilities, and receivables should all be modelled the same way, as a definition entity plus a time series of valuations. That insight was not in the original design. It emerged from running the schema against real data.
Project#
Five days. Three services live on Dev. Seafile, Immich, and a financial stack with a database schema that took longer to design than to deploy.
Two recurring patterns worth naming.
The first: credentials in chat. Three times during this week, a secret appeared in a chat window — a database password, an API key, a token — because I pasted a configuration file without masking it first. Each time the secret was rotated immediately. Each time the lesson was the same: use sed to mask values before pasting anything that might contain credentials. This is not a difficult habit to build. It requires deciding in advance that the rule applies even when you are moving fast.
The second: the SanDisk. The backup drive had been physically attached to the Pi since Phase 0. It had never been added to /etc/fstab. This meant that after every reboot, the drive was not mounted, and the backup would have run silently against a missing path. The fix was a single line in fstab with a UUID and a nofail flag. It took five minutes. The oversight had been present for weeks.
Both patterns point at the same thing. Operational discipline is not about the hard problems. It is about the small things that are easy to defer because they are not urgent. Until they are.