27 August 2026 · Updated 9 October 2026
This site, the private hub behind it, and how a personal website that started out simple grew into the thing our household runs on.
Through the Cloudflare tunnel
Visitors
Four domains, one app
Public site
Next.js. Renders published content only. Four environment variables, a read-only view of the files, no admin code at all
LAN or VPN only, never tunnelled
Me
A password and an authenticator code, or a passkey
Admin console
Content, files, smart home, energy, cameras, local area, server health, backups
Shared underneath both
Content store
JSON files and uploads on disk, written atomically. The public site can only read it
Data service
Headless. Polls every source, keeps history, answers questions
Postgres
Readings, trends, settings, audit logs
Polled by the data service
The public site and the admin console are built from the same repository and share the rendering code, so a draft previews exactly as it will look live, but they run as separate containers and only one of them is ever reachable from outside. That one has no port of its own: it sits on a private network that only the tunnel can reach, with no route back into the house. A camera’s privacy state, a bin day or a bus time all arrive the same way: a poller, a reading, a row in Postgres.
This is my personal site, and it started life as an argument with my own work.
I also build Wanating CMS Core, a content platform for small organisations. I wanted my own site to prove the opposite point: that a personal website doesn't need a platform, a database or an admin panel to be easy to maintain. It was four small Next.js apps, this site plus a marketing site each for Armarium, Dægwork and Core, sharing one design system and one Docker image. There was no CMS at all. To change a sentence, an AI coding agent edited the file, ran a real production build, and I redeployed.
It was lovely, and it didn't last.
The first crack was the other three sites. Four apps meant four containers and four deploys, for what were really four pages. So I taught this app to answer for more than one domain. A small table maps each domain to one of its own pages, and armarium.app, daegwork.app and core.wanating.uk became pages inside this app. Deleting three whole codebases in one go is a very satisfying commit.
The second crack was me. I wanted to fix a typo from my phone without opening a code editor. So the site got a content store: pages and posts made of blocks (text, images, galleries, callouts, quotes), kept as JSON files on disk. Every write is atomic, so a half-finished save can never corrupt anything. Posts can sit as drafts for days and only go live when I press Publish. The diagrams on these pages are "widgets": small components owned by the code, which I can place like any other block but don't edit through a form.
There's still no database behind any public page, though there's one behind almost everything else.
The editing screens lived at /admin, inside the same app that serves the public site. The only thing between them and the internet was a login page. That's fine until you think about it for long enough, and then you can't stop: one misconfigured tunnel rule would put my whole admin area on the internet.
So it moved out. Admin is now a separate app, in a separate container, on a port the public tunnel doesn't know exists. It's only reachable from inside the house or over the VPN. Both apps are built from the same repository and share the code that renders content, so a draft previews exactly as it will look live.
The public app lost every secret it had no business holding. About all it has left is a token that can record page views and nothing else.
Once there was a private, logged-in place to put things, I kept finding things to put there. I wrote about how that happened in When AI became part of the architecture. The short version: the console became the hub, the front door to how our house runs.
Underneath it sits a small headless data service with its own Postgres database. Everything third-party flows through it: Home Assistant, my energy supplier's API, Plex and Music Assistant, bus and train data, the council's bin schedule, weather and flood warnings, road closures, and reports from the server itself. Each source has a poller, each poller writes readings, and every page reads summaries rather than raw history.
That last part came from a real production incident. The Energy page was downloading every meter reading ever recorded, 4.4MB of JSON on every visit, until it timed out and the console started reporting perfectly healthy services as down.
Today it's organised around how the household actually uses it:
It started with one login. Now the family has their own, which raised the bar: it's held to a higher standard than most things I've built for thousands.
Everyone has their own account, by invitation, and can see and end their own sessions. Every page and every action checks a permission first, and that isn't left to memory: a test fails the build if a page, route or action is ever added without one. A person's private files are private from me too, even though I run the server. And nobody's location is shared without their consent, which I can't override.
Every page gets a Content-Security-Policy with a fresh nonce per request, and no 'unsafe-inline' or 'unsafe-eval' anywhere. No third-party scripts, no webfonts, no trackers: a page on this site makes no requests to anyone else. Containers run as non-root users on read-only filesystems. Logging in needs a second factor, repeated failures lock the account for longer each time, and changing anything about how you log in asks you to prove it's you first.
The public site has two API routes, and neither can read or check anything itself: they only pass things on. One exists for our phones, which post their location to it for the hub. The other takes roadworks notices pushed by the government's Street Manager service. The checking all happens behind them.
The September audit I describe on the Home lab page fixed most of the rest. Those fixes are the least glamorous work on the site and, to me, some of the most satisfying.
Two favourites, because the fixes taught me more than the features did.
Safari's silent clicks. Links in admin would sometimes just not work in Safari. No error, nothing in the console. The request went out, the change was saved, and the page simply didn't update. It took three attempts on one clickable card before I stopped fixing symptoms and found the cause. This site generates a fresh security nonce for every request, and Next.js's client-side cache was reusing pages rendered with an old one. The browser, quite rightly, refused to run them, and said nothing about it.
The fix came in two layers: turn that cache off, then use real browser navigations for admin links. I proved it with a script that clicked links hundreds of times in real WebKit and counted how often they landed. I tested it again recently. The framework's own links are reliable now, but still six times slower than a plain page load, so the workaround stays.
The 404 that returned 200. Unknown pages showed "Not found" with a success status code, which search engines treat as a real page. The culprit was a single loading spinner at the top of the app. Once part of a page starts streaming, the status code has already been sent, and it's too late to say 404. Every section now checks the page exists before its spinner starts.
A pnpm monorepo: Next.js 15 and TypeScript for both apps, Tailwind CSS 4 for the design system, Postgres for the data layer, and one Dockerfile shared by every app. The tests are narrow on purpose. They cover the pure logic where a silent mistake would hurt most: path traversal checks, upload validation, rate limiting, second-factor codes, drive health rules.
Almost all of it was written pairing with Claude Code. My job is architect, reviewer, and the person who says "no, that's not what I meant."
Each area of the codebase has its own document explaining what it does and why, what was tried, and what was rejected. Whoever picks it up next, me or an agent, starts from understanding rather than guesswork. The server's rebuild runbook lives in the same repository and copies itself into the site's private file store on every deploy, so it's backed up along with everything it describes.
So today there's a Next.js app serving four public domains with no database behind any public page, a private hub that has quietly become how our household runs, and a data service collecting everything worth knowing, all on a server that's backed up, monitored and documented well enough to rebuild from a blank disk. The Home lab page covers that side.
It's a long way from "four static sites and no admin panel". I don't think the first instinct was wrong, though. Everything here earned its place by solving a problem I actually had, and it's still where I find out how things work.