Craig Wright
HomeAboutProjectsHome labMusingsContact
© 2026 Craig Wright
← Musings

27 August 2026 · Updated 9 October 2026

Wanating

This site, the private hub behind it, and how a personal website that started out simple grew into the thing our household runs on.

Wanating

Public

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

Private

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

Data

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

Home AssistantEnergy meterBuses and trainsBinsWeather and floodsRoadsThe server itself

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.

Simple on purpose

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.

From files to a content store

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 admin Studio editor: a post's blocks in order, one expanded to edit its text
Studio, editing this post. Drafts save as you go and only go live when published.

The admin console grew up, and moved out

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.

A personal operating system

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:

  • Today: one page that says what needs attention and what's going on, from bin day to whether the server's disk is filling up.
  • Media: music, audiobooks, films and TV in one place, playing to speakers from three different makers, bit-perfect. Films merges what's on Plex with the discs on the shelf.
  • Library: a private file store with time-limited share links, and a PDF reader that behaves the same in every browser.
  • Home: lights, per-room climate, batteries, cameras and presence, an energy dashboard that works out what things actually cost, and the local area: bin days, live buses with a record of how reliable each route really is, trains, roads on a self-hosted map, weather and flood risk.
  • Operations: resource trends, what's using the server, drive health, pending updates, backups with a recovery readiness check, alerts, and a daily report by email.
  • Website: this site's pages, posts and visitors, including posts that go live on their own on a set date.
  • Phones: an Android companion app for the family, with alerts and reminders, the media library (downloads included, for the train), and home screen widgets.
The hub's Media page: recently played and recently added albums with CD and Digital badges, and a now-playing bar marked bit-perfect
Media: one library, any speaker in the house, and a now-playing bar that says when playback is bit-perfect.

Security for a household

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.

Bugs worth the telling

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.

How it's built

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.

Where it stands

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.

The hub's Films view: 223 films as poster tiles, filterable by Blu-ray, DVD, Digital and Offline
Films: what's on Plex and the discs on the shelf, in one catalogue.