Thursday, July 30, 2026

DigitalOcean Gave Me Less Than a Month. Then React Router Made Me Upgrade Three Major Versions at Once.


I'm a solo student developer. I built Naval PM, a real project-management tool with real signed-up users, on a $200 DigitalOcean credit from the GitHub Student Developer Pack — advertised, like every year before it, as valid for one year from signup.

I created my account on July 4, 2026.

What I didn't know: DigitalOcean had already started winding the whole partnership down. On June 8, they quietly cut what the credit could be spent on. On June 12, they started emailing existing pack users that the program was ending, with every credit — no matter when it was redeemed — expiring August 1, 2026.

I signed up after that email went out, so I never saw it. What I did see was a redemption page that, as far as I could tell, still said "valid for one year." Other students who redeemed in that same window later found their actual billing page showed the real expiration — August 1 — nowhere near what the page had promised. Some only found out by asking support directly.

July 4 to August 1 is less than four weeks.

That's not a 60-90 day courtesy notice. That's not even the year I signed up expecting. That's "the marketing page and the real terms were already two different things by the time I hit redeem."

So I migrated. Fast.

I moved everything to Oracle Cloud's free tier: 1 shared vCPU, 956MB of RAM, a 45GB disk. It's small — genuinely, uncomfortably small for a Node + Postgres app with real users — but it's mine, and nobody can pull it out from under me with three weeks' notice.

I hardened it properly: three layers of firewall (Oracle's own NSG, ufw, and iptables), real Let's Encrypt TLS, and a 2GB swap file tuned so real RAM gets used first and swap is a safety net, not routine behavior. On a box this small, that last part is the difference between "a little slow" and "the OOM killer just ended your Postgres connection mid-request."

I thought that was the hard part. It wasn't.

Then GitHub flagged a "High" severity CVE

Right after finishing the migration, a Dependabot alert showed up:

React Router: RSC Mode CSRF Bypass Allows Action Execution Before 400 Response Severity: High (7.1) · Patched in react-router@8.3.0

The instinct is to panic-patch. I didn't — I checked first. The CVE only applies to React Router's unstable RSC (React Server Components) mode. My app uses plain, stable BrowserRouter / Routes / Route. Not exploitable, technically. I could've closed the alert and moved on.

I didn't. I decided to actually take the upgrade — which turned out to be a much bigger decision than it sounded, because react-router@8.3.0 requires React ≥19.2.7. One "patch a CVE" ticket became "upgrade React, react-router, and everything downstream of both, on a live app, on a box with less than 1GB of RAM."

What that actually took

  • Auditing every library that touches React to see what would actually break: react-leaflet (v4→v5, checked the one map component that uses it), resend (the email SDK — jumped 3→6 mainly to drop a dependency pinned to React 18, then double-checked the actual send API hadn't changed shape), recharts and lucide-react (deliberately held back from their newest majors — no reason to stack more risk than the upgrade already required).
  • Rewriting 29 files' worth of imports, because React Router v8 folded react-router-dom into react-router and stopped publishing it separately.
  • Catching a genuinely nasty gotcha: the upgrade had been prepared in a separate work session, from a snapshot taken before I'd hardened my production config (a capped database connection pool, Docker memory limits). Merging it in silently reverted all of that hardening — no error, no warning, just a quiet regression sitting in the diff. Had to catch it by explicitly diffing the exact files I knew I'd touched before, not by trusting a clean git status.
  • A full verification pass before deploying anything: clean install, clean production build, 348 passing tests across 37 suites, clean security/audit checks in CI — and then, because none of that actually proves a browser renders the page correctly, an honest manual click-through of the live site.
  • A ~10-minute container rebuild on a single shared vCPU, because bumping the Docker base image from Node 20 to Node 22 invalidated every cached layer.

It worked. Zero downtime, zero broken pages, and — as a nice side effect — the original CVE alert and its matching Dependabot PR both auto-closed themselves the moment the fix landed on master.

The actual lesson

None of this — the migration, the hardening, the forced three-library upgrade — was optional or was work I chose to sign up for on July 4. It was the cost of building on infrastructure I didn't control, with terms that could (and did) change under me with barely any notice.

If you're a student building something real on free credits: back up everything, assume the free tier is a loan that can be called at any time, and budget real hours for "unplanned infrastructure work" into every project — because eventually, something you didn't choose will force your hand, usually at the worst possible time.


Naval PM is a project management tool I built and run solo. If you've been through a similar forced migration or upgrade, I'd genuinely like to hear how it went for you.

Thursday, July 16, 2026

The Anatomy of a "Trivial" Dependency Update

Dear readers,

Every week, NAVAL-SEM — my free, offline SEM (structural equation modeling) desktop app — gets a batch of Dependabot PRs. Six of them showed up this week. All patch or minor version bumps. All green checkmarks. The kind of PR most maintainers merge without a second look.

I decided to actually test them first. Here's what I found instead.

Step 1: Zero tests collected

Before touching any dependency, I ran my test suite locally to establish a baseline.

collected 0 items
no tests ran in 0.24s

My test suite has 200+ tests. Running pytest from my actual repo directory found none of them. It turned out my tests lived in a separate local validation folder — kept out of the public repo deliberately, since the test cases encode methodology I don't want competitors reverse-engineering. But that folder had never been symlinked or copied into the actual working checkout. I'd been "running my tests" against an empty directory for who knows how long.

Fix: copied the tests into the repo under a gitignored tests/ folder, so they run locally without ever being pushed publicly.

Step 2: A resolver conflict hiding behind CI's green checkmark

With tests actually discoverable, I ran uv sync --upgrade to pull in the pending bumps. The resolver refused:

× No solution found when resolving dependencies
  ╰─▶ Because pydantic==2.13.4 depends on pydantic-core==2.46.4
      and your project depends on pydantic-core==2.47.0,
      we can conclude that your project's requirements are unsatisfiable.

Two lines in pyproject.toml — a direct pin on pydantic and a separate direct pin on pydantic-core — had drifted out of sync. pydantic-core isn't meant to be pinned directly at all; it's a transitive dependency pydantic manages internally. An earlier Dependabot PR had bumped pydantic-core on its own, without anyone (bot or human) checking whether pydantic itself still agreed with that version.

CI never caught this because CI only validates the diff in a given PR — not whether the cumulative state of all your pins is internally consistent.

Fix: dropped the redundant pydantic-core pin entirely and let pydantic manage it.

Step 3: A 200-test regression run, live server included

Because my tests are integration tests that hit a running instance of the app over HTTP, "run pytest" first meant "start the app in a separate terminal." Once that was in place, the full suite ran clean — twice, once for each dependency batch (narwhals + tzdata + websockets + anyio, then setuptools separately since it's a major version bump).

201 passed, 2 skipped in 805.55s

Thirteen minutes per run. Worth it — this is exactly the run that would have caught a real regression, and it's the run a solo maintainer is most tempted to skip when the CI badge already looks green.

Step 4: Chasing a log line into a dead dependency

Somewhere in the startup logs, a line caught my eye:

No graphviz package found, visualization method is unavailable

I don't call graphviz anywhere in my code. Tracing it down, it turned out to be semopy — a hard dependency for my SEM engine — silently attempting to import graphviz at module load time, for its own optional semplot() diagram function. I've never called that function. My actual model diagrams are rendered entirely client-side: a D3/SVG canvas, screenshotted via html2canvas, and shipped to the backend as a base64 PNG for report exports.

Graphviz had been sitting in my dependency tree as dead weight — a native binary with per-OS packaging quirks that I'd have had to bundle across three release platforms (Windows, macOS, Linux) for a feature I don't use.

Fix: removed the graphviz Python package and uninstalled the system binary. Nothing broke, because nothing was using it.

Step 5: The lockfile that quietly drifted

Last thing: after merging the narwhals and setuptools PRs through GitHub's UI, my local uv.lock and the committed one on master disagreed — the PRs had only touched pyproject.toml, not the lockfile. Anyone doing a fresh uv sync --locked clone would've hit a resolution mismatch that I wouldn't have seen locally, because my own environment had already been synced ahead of the commit.

Fix: regenerated and committed the lockfile in its own PR.

The takeaway

None of these six problems showed up as a red X anywhere. Every PR CI ran was green. Every one of these issues was found by actually running the software, not by trusting the badge.

If you maintain free software — even something small — this is the tax nobody sees: an hour of archaeology behind every "trivial" merge, so that the next person who clones your repo doesn't inherit a broken environment.


Thank you for reading,

N. Singh

DigitalOcean Gave Me Less Than a Month. Then React Router Made Me Upgrade Three Major Versions at Once.

I'm a solo student developer. I built Naval PM , a real project-management tool with real signed-up users, on a $200 DigitalOcean credi...