The 5N3 Domain
fiventhree.com and everything under it: home page, portfolio, this hub, the tracker and the vault. One design system, one VPS, managed end to end. Live since 30 July 2026.
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:
| Site | What it is |
|---|---|
| fiventhree.com | Landing page for the 5N3 practice |
| profile.fiventhree.com | Portfolio: experience, skills, certifications, CV |
| blog.fiventhree.com | This hub |
| tracker.fiventhree.com | The Tracker app |
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.
This hub#
The site you're reading is a zero-dependency static generator: markdown files in, HTML out, built by one Node script with no packages to install. Search, table of contents, callouts, terminal blocks, and Mermaid diagrams all work without a framework.
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.
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
Roadmap
- One design system across all four sitesShipped
- Knowledge base rebuilt around projects and topicsShipped
- One shared header chassis across all four sitesShipped
- Whole-domain deployment runbook and deploy scriptShipped
- Pre-deploy security reviewShipped
- Live on a VPSShipped
- all four subdomains over HTTPSNext
- Nightly backups pulled off-site and restore-testedShipped
- Architecture write-up with diagramsShipped
- Fifth subdomainShipped
- a self-hosted password vault on the existing PostgresNext
- Mail on the domainShipped
- addressed routing that the DNS was explicitly configured to refuseNext
- Uptime monitoring that tells me before a visitor doesNext
- robots.txt and sitemap polishNext
- Move Caddy into its own project so each product has an external networkNext
Development log
Every article written against this project, newest first.
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.
Building this blog
Why this journal is a folder of markdown files and one build script, with no framework, no database and no dependencies.
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 moreMail 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 moreLaunched 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