Field notes from a hosting migration for an NHS food-diary proof of concept. A certificate checker
was quietly lying about whether a fix had worked, and two sessions rejected the same hosting idea
twice, four days apart, without either reading the other's reasoning. The move itself was still on
hold when this was written.
This is a proof-of-concept, not an NHS product, built by one technical person working alongside an
NHS dietetic team. No patient data exists anywhere in it. The app stores everything on the
client device and nothing else. The trigger for all of this was mundane: Netlify's free tier caps
a project at 15 credits per production deploy, 300 a month, which works out to roughly twenty
deploys before the meter runs dry. The project was already rationing pushes against that ceiling,
and shared hosting on Namecheap, already being set up for another project the same person runs,
looked like a way out.
Two rejections, four days apart
GitHub Pages came up early, and the project turned it down fast. On the free plan it only serves
public repositories. A private repo needs a paid GitHub plan, and even then the site still
publishes to a public URL with no login wall short of Enterprise. Netlify's free tier deploys from
a private repo to an unlisted one instead, which fit the stated preference to keep the code itself
private. No patient data was ever going to sit in that repo either way. The preference was about
the source, not about anything a patient had typed.
Four days later, GitHub Pages came up again. Not from a different person with a different view. A
coding session working on the project proposed the exact idea an earlier session had already
rejected, because nobody had reread the decisions file before speaking. The occasion was almost
incidental: a different, newer domain had surfaced, already live on GitHub Pages with a working
certificate, and for a moment it looked like an obvious destination. That session looked into it,
then declined it again, for the same reason as the first time. That reason had not moved. It had
simply never been reread. This round did turn up one genuinely new blocker: the food-diary repo
and the newer domain lived under two different GitHub accounts, and GitHub blocks a custom domain
from being claimed by a second account once it's verified on the first, specifically to stop that
kind of takeover. A real finding, arrived at by accident, in the course of re-litigating a question
that had already been settled.
A decisions log nobody rereads is decoration. It was there, it had the right answer sitting in it,
and a session still asked the question again anyway.
A certificate checker that had been lying
The SSL side had its own real bug and its own fake one. The real bug: the domain's nameservers
were pointed at Namecheap's basic DNS, which only handles URL forwarding, not actual hosting.
Switched to the proper hosting DNS and that half resolved. An earlier assumption, picked up from a
web search during planning, claimed the hosting tier included self-service AutoSSL. It didn't.
Triggering AutoSSL turned out to require WHM-level (the hosting provider's own backend) server-administrator access, which an ordinary
shared-hosting account never gets. That was confirmed against Namecheap's own documentation once
the free "Run AutoSSL" button turned out not to exist anywhere in the dashboard. The free certificate that does ship
with the hosting plan has to be issued from Namecheap's own side, and nothing on the customer's end
can force it.
The fake bug was worse, because it looked like a real one for a while. This laptop's own antivirus
intercepts TLS connections and reissues every certificate itself, so anything running on the
machine, curl and openssl included, was reporting the antivirus's own certificate instead
of the real one.
The tool being used to answer "has the SSL fix landed yet" could not tell the truth about it even
in principle.
Checking from a phone worked around it, and later a tool that runs the request server-side,
outside the interception entirely, confirmed it for good. Two support agents tried
force-validating the certificate from Namecheap's own side with no result, and a follow-up ticket
went in to track it as a pure backend wait. Nothing on the customer side could shortcut that part.
The certificate issued not long after, and a server-side check confirmed clean HTTPS.
Two things the plan hadn't accounted for
First: SSH turned out to be off by default on shared hosting, though the migration plan had been
written without confirming that. The fix was self-service, a control panel option called "Manage
Shell," which gives no hint it's SSH, listening on a non-standard port rather than the usual one. Second:
pushing to the new remote from an automated shell hung indefinitely with no error, because a global
credential setting that keeps two separate GitHub accounts from colliding also blocks the plain
per-repo push path unless a repo-level override is set. That global setting has to stay exactly as
it is. The only new part is the missing per-repo override.
A smaller trap surfaced once both hosts were finally level with GitHub again: the control panel's
Git tool has an "Update" button that only saves configuration. It doesn't fetch, pull, or deploy
anything, and those actions live on a separate tab entirely. For a repository that was deliberately
cloned through the control panel's own "Clone a Repository" tool, with a specific option left unchecked to
preserve its automatic deploy hook, even that tab's own "Update from Remote"
option turns out to be a no-op. It's a push target, not a pull source. Reading the UI correctly
took longer than reading the DNS panel had.
Deleting a task is not a local edit
One step in the migration plan retired Netlify outright once the new host was live. Once Namecheap
turned out to be an interim stop rather than the final destination, the task was dropped, which
seemed like a small, contained change. It wasn't. That same task had been referenced as a
precondition in five other places across the plan, and one of those was dangerous on its own: a
later step told a future session to delete the Netlify credit-cap constraint from the project's own
guide file, once hosting had moved. Hosting hadn't moved. A session following that instruction
literally would have silently removed a still-live limit from the record that exists to track it.
All five references were found and corrected before that could happen.
What actually changed the private-repo rule
The preference that had driven both GitHub Pages rejections wasn't overturned by a technical
argument. It was relaxed on purpose, once the user actually named a real gap instead of waving it
off: the diary's free-text fields, a description typed next to a photo, have no structural way to
stop someone entering something identifying. Going public meant accepting that gap rather than
pretending it away. In the user's own words, it was "hard to guard... loose enough to say go
public." The account-split blocker from the second rejection turned out not to matter here either,
since the plan kept the repo under its existing account rather than moving it to the newer brand's.
A bigger swap, investigated instead of just refused
A smaller question, whether a branded subdomain could dodge a shared hosting username, led
somewhere larger: making the newer, higher-traffic domain the account's designated "Main Domain,"
on the assumption that the label itself would grant it more subdomain headroom. That assumption
didn't survive a check. The account's subdomain quota is shared regardless of which domain holds
that label, so an ordinary addon domain gets exactly the same headroom. What an
actual swap would require turned out to be much bigger than the label suggested: a different
project's own blog would have to leave its free, automatic hosting for manual shared-hosting
deploys with none of that automation and the same multi-day certificate process already documented
here. That's not a call this project gets to make about another one. It was declined without
touching anything, and the idea went out as two information-only notes to the two other projects'
own owners, for them to decide on their own terms.
How it actually landed
The reversal, when it came, was decided outside any of these sessions, by the one person who owns
all three projects at once: Netlify was retired entirely, and the food diary moved to GitHub Pages
after all, kept under its existing account rather than a newer pseudonymous one. That keeps a tool
built under a real identity separate from a brand built under a pseudonym, and it sidesteps the
cross-account domain restriction outright rather than working around it.
The account question that had paused the move turned out simpler than the plan in flight at the
time suggested. Rather than standing up a new account, the existing one kept its history and its
repos exactly where they were and simply changed its own display name. Same account throughout,
just renamed partway through. The migration is complete now. Both old hosts are fully retired, and
nothing is left dual-hosted.
It ended without a clever fix. Two things went wrong here that weren't code. Both were caught the
same way, by someone who actually checked instead of assuming. A certificate check that had every
reason to look trustworthy was quietly answering a different question than the one being asked. And
a written decision only held once someone actually reopened the file before speaking a second time.
Originally published at thekilted.dev/nobody-reread.
Top comments (0)