5N3BLOG ← 5N3
Live Pinned

The 5N3 Domain

fiventhree.com and everything under it: home page, portfolio, this

StatusLive
StartedJun 2026
Last updateOct 2026
Updates3

How this happened#

None of this was planned. It's the third step in a chain where each step made the next one seem obvious.

The media server taught me self-hosting. Self-hosting led to the Tracker, a real app to replace the Excel sheets I'd been tracking my life in. And then the Tracker needed somewhere to live, which meant a domain, which meant a landing page.

That should have been the end of it.

Instead I discovered that designing and building these things is genuinely, uncomfortably fun. One landing page became a portfolio. The portfolio needed a place to write things up, so that became this knowledge base. Then everything needed a shared design system so it didn't look like four unrelated websites wearing the same domain.

It spiralled, and I let it, because every evening spent on it taught me something and none of it felt like work. What started as somewhere to put the app turned into a set of products with a real design language, which as it happens is a far better thing to show someone than a single app would have been.

Overview#

Everything I ship lives under one domain, on infrastructure I run myself:

SiteWhat it is
fiventhree.comLanding page for the 5N3 practice
profile.fiventhree.comRecruiter-facing portfolio, see below
blog.fiventhree.comKnowledge base, its own project page
tracker.fiventhree.comThe Tracker app
vault.fiventhree.comSelf-hosted password manager, see below
cms.fiventhree.comGitHub OAuth bridge and deploy webhook behind the Blog's editor

One design system#

Every site pulls the same tokens from a single canonical stylesheet: matte black surfaces, one red accent, Inter, 16px card radius. Change a colour once and the whole domain updates. The tracker is the reference implementation; a sync script pushes the tokens out to the static sites.

The vault#

The fifth thing under the domain, and the first one not written here: Vaultwarden, a well-regarded Bitwarden-compatible server, at vault.fiventhree.com since 3 August 2026. It runs on the same PostgreSQL instance as the Tracker, in its own database, so the nightly pg_dump, the thirty-day retention and the restore test that already existed for the Tracker covered it from day one.

Items are encrypted in the browser, with a key derived from the master password. The master password never reaches the server, so Vaultwarden stores ciphertext only, and there is no reset: no recovery email, no admin override. Lose the master password and the vault is undecryptable blobs. That fact decides everything else here. Signups, invitations and the admin panel are all closed, verified closed from a private window, since this is a one-person vault with nobody to invite and no settings worth leaving an open door for.

The CSP on this one origin is tighter than anywhere else on the domain: connect-src 'self', frame-ancestors 'none', Cache-Control: no-store. One real cost of that: the web vault's breach check against api.pwnedpasswords.com is blocked by connect-src 'self', and the failure is silent, the signup button simply does nothing. Left blocked on purpose. The breach check is a convenience; the CSP protects the one thing on this infrastructure with no recovery path.

The portfolio#

A single-page profile at profile.fiventhree.com, built for one audience: someone deciding whether to interview me. Experience across military and hospitality IT, certifications with status, project highlights, education and documents, everything a recruiter scans for, in one scroll. Plain HTML, CSS and vanilla JavaScript, no framework and no build step, with all content in one data file so a title change or a new certification is a one-line edit. It originally shipped with its own blue and purple identity before being restyled onto the same matte-black, one-red-accent system as everything else here, so the whole domain reads as one product rather than four unrelated sites sharing a name.

Deployment#

Live since 30 July 2026. A single VPS runs the tracker's containers; Caddy fronts everything, serving the three static sites directly and reverse-proxying the app, with one config block per subdomain and automatic HTTPS for all of them. DNS at Cloudflare, deliberately unproxied.

SSH is key-only with root login disabled, the firewall allows three ports, and Postgres publishes none at all. Deploys are a git pull and one script; the static sites update as a file copy with no downtime.

The database is dumped nightly, pulled off the server to a second machine each morning, and the restore is tested rather than assumed. The pull warns loudly if the newest dump is more than 48 hours old, because the realistic failure is a backup job stopping quietly, not crashing.

The full write-up, including the four things that broke on the way, is in Four sites, one VPS.

What running it has taught me since#

Three things have gone wrong since launch, and all three failed in the same shape: every individual component was correct, and the fault was in the gap between them.

A changed config file does not reach a running process. The Caddyfile is bind-mounted, and docker compose up -d only recreates a container when its spec changes, not when the contents of a mounted file do. So git pull updated the file while the running Caddy carried on with what it had read at startup. Adding the vault failed its TLS handshake with the container healthy, DNS correct and the file on disk right, because the loaded config had no block for the new name and therefore never requested a certificate for it. The deploy script runs caddy reload now.

A backup script that fails to parse is worse than one that fails to run. The script that pulls dumps off the server to a second machine acquired a PowerShell syntax error: @(foreach ... | ...) is a parse error in Windows PowerShell 5.1, where @(foreach ...) | ... is fine. PowerShell reads the whole file before executing any of it, so the script didn't fail partway, it failed to exist. Every nightly run afterwards did nothing. The staleness warning designed to catch exactly this kind of silent stop was inside the file that wouldn't parse. Off-site copies fell weeks behind while the server's own dumps were perfect.

The check has to run automatically or it isn't a check. Both of the above, plus a class of API errors in the Tracker, now have guards wired into deploy.sh rather than into a habit: it refuses to deploy when a response model doesn't match its query, and it reloads Caddy every time instead of trying to detect whether the config changed. Detection is the part that rots.

Mail, and the records that said no#

The domain carried four records whose entire job was to declare that it sent and received no email: a null MX, an SPF record authorising nobody, an empty DKIM wildcard, and DMARC set to reject. That is good hygiene on a domain going on a CV, because without it anyone can forge mail from it and a recruiter's mail server has no way to tell.

It also means turning mail on is mostly a deletion job, and the two that had to go were the two that fail quietly:

- A null MX must be the only MX record. Adding routing MXes beside it does not override it. Priority 0 outranks everything, so senders keep being told the domain accepts no mail while the dashboard shows green. - A domain may publish exactly one SPF record. Two is a permanent error, and the safe reading of a permerror is failure.

The DKIM wildcard stayed, and that part is neat: a wildcard is only consulted when no more specific record exists, so the provider's real selector answers for itself while every unused selector still says "no valid key". The anti-forgery property survives the change intact.

Receiving works. Sending does not yet: forwarding accepts mail addressed to the domain but does not let you send as it, so replies still leave from a different address until an SMTP relay is wired in. Worth knowing before putting the address on anything.

Keeping the stack current#

Node 20 reached its own end of life in October 2026, the same month a real Node security release fixed ten CVEs in the 22.x, 24.x and 26.x lines and explicitly left 20.x unpatched. The host and both frontend containers that still ran it moved to Node 24 the same week, ahead of anything exploiting the gap, written up in Checking fiventhree.com for the October 2026 Node CVEs.

The Blog also gained a real editor that month: Decap CMS, its own project page now covers the full story.

Roadmap#

- Uptime monitoring, so I hear about an outage before a visitor does - robots.txt and sitemap polish - Sending as the domain address, rather than only receiving - Move Caddy into its own project, so the vault stops living in the Tracker's compose file and each product gets an external network

Development log

Every article written against this project, newest first.

Checking fiventhree.com for the October 2026 Node CVEs

A routine ask: make sure nothing on the VPS was exposed to recent Node.js or PHP CVEs. There was no PHP to check, and Node turned out to be running an end-of-life line that a real security release had just skipped over.

A full security audit of fiventhree.com, and the bug it found

An external and internal audit of the whole domain, five subdomains in. Most of it held up. One header fix turned out to be silently ignored in production for a reason worth knowing about.

Four sites, one VPS: how fiventhree.com actually got deployed

The whole domain went live on a single server in one evening. Here is the topology, the order I did things in, and the four things that broke, including the one where three sites returned 404 while the fourth worked perfectly.

Timeline

Guardrails moved into the deploy script

Two production 500s and a failed release traced to the same thing - a check that only existed as a habit. The deploy now refuses to ship when a response model doesn't match its query, and reloads Caddy every time rather than detecting whether the config changed.

Read more

Mail on the domain

mohamednawilnahid@fiventhree.com now routes through Cloudflare Email Routing. The interesting part was what had to be removed: the domain published a null MX and a blanket SPF deny saying it accepted no mail, and both had to go before anything could arrive.

Read more

Self-hosted password vault

Vaultwarden at vault.fiventhree.com, on the same Postgres as the Tracker so one nightly dump and one restore test cover both. Signups and admin panel closed, and a tighter CSP than any other origin on the domain.

Read more

Launched fiventhree.com

Live on 30 July 2026. All four subdomains serving over HTTPS from a single VPS I run myself - DNS, firewall, certificates, deploy script and off-site backups end to end.

Read more

Portfolio rebuilt for recruiters

v2.0 - executive summary, skill matrix, certification roadmap and a single master CV.

Read more