I Migrated My Site to Cloudflare from My Phone

At 7:51 this morning I forwarded my AI assistant a screenshot of an email from Vercel. My free team had used 75% of its 1M CDN request allotment.

My first question was the obvious one: what happens if I do nothing? The answer was worse than I assumed. On the free plan there is no monthly reset. Cross 1M requests and Vercel pauses your projects, in most cases for 30 days, unless you upgrade to Pro. The site in question is llm-digest.com, the AI news feed I have been building in public since February (building it, splitting the pipeline, fixing what the split broke).

Doing nothing was fine at 75%. But the number was only going up, so at 7:54 I asked the question that decided my morning: maybe migrate? To Cloudflare?

750K requests, almost none of them readers

The first useful thing the assistant did was read the repo instead of guessing. Real reader traffic was around 1.6K pageviews a week when I last measured it this summer. The 750K requests are overwhelmingly bots and crawlers hitting static pages.

That changes what the cap actually costs. Vercel’s free tier counts every request against the same limit, so a crawler fetching a static HTML file costs the same as a reader fetching it. Cloudflare does not meter static assets at all. The only thing that counts there is Worker invocations, 100K a day, resetting daily. My eleven small API endpoints would need to get sixty times busier to matter.

The alternative was Vercel Pro at $20 a month, which is zero work and buys a bigger bucket for the same leak. Moving made the limit stop mattering instead of raising it. I said go.

What the move actually involved

The site was a good candidate, which is part of the story. Static rendered HTML plus eleven small Node serverless functions under /api, with a GitHub Action that rebuilds the feed data every two hours and pushes it to main. The plan the assistant came back with:

  • The static pages move as-is to a Cloudflare Worker’s static assets. No changes.
  • The eleven /api functions get converted to Worker fetch handlers. The one real problem: they read JSON from disk (latest.json is about 100KB, judgments.json about 3.8MB), and a Worker has no disk. The data moves to R2, Cloudflare’s object storage.
  • The same two-hourly GitHub Action that already publishes the data gains two steps: sync the changed JSON to R2, then deploy the Worker. The publishing pipeline I already had becomes the deploy pipeline.
  • The mechanical porting details: node:crypto to Web Crypto, env vars to wrangler secrets, and parsing big JSON once at module scope instead of per request, because the free tier gives you 10 milliseconds of CPU per request.

The two hours

Here is the timeline, because it is the part that still surprises me.

  • 7:51 - I forward the warning email and ask what happens if I ignore it.
  • 7:58 - I say go on the migration.
  • 8:21 - The patch is ready: all 23 tests passing, every page and endpoint verified against the live site.
  • 8:35 - A preview is live on a workers.dev URL. My production site is untouched.
  • 8:40 - The real data is seeded into R2 and the preview is a full working copy.
  • 8:59 - Email signup verified end-to-end with a throwaway address.
  • 9:39 - Branch pushed, PR open, auto-deploys wired: every push to main now syncs data to R2 and redeploys the Worker.
  • 9:47 - I merge the PR and the cutover runs: the two Vercel DNS records come out, both custom domains attach to the Worker, and the live domain gets checked page by page.
  • 10:01 - Done. llm-digest.com serves from Cloudflare.

Two hours and ten minutes from a warning email to a finished production migration, and I never opened a laptop. The whole thing ran through iMessage, including the parts where credentials had to change hands. That part deserves its own section, because it is what made the phone enough.

What was left for me

My job compressed down to a short list: decide it was worth doing, create four API tokens, click one “enable R2” button, pull one key from my Resend dashboard, and say the words “flip it” when I was ready.

Every irreversible step waited for my word. Merging the PR was safe anytime. The domain flip waited for “flip it.”

How the secrets moved

The obvious way to get this wrong is pasting API tokens into a chat window. No secret ever went through the chat.

Each time the assistant needed a credential, it sent me a secure link. I created the token in the provider’s dashboard, dropped it into the link, and it landed in an encrypted vault the assistant could use but never show back to me. In order: a Cloudflare API token, an R2 access key pair, a Vercel API token, and a GitHub token. The GitHub one I dropped twice, because the auto-deploy workflow file needed the workflow scope my first token did not have, so I added the scope and dropped it again.

The one secret the assistant could not pull over itself was my Resend API key. Four of the five email env vars came across from Vercel with the API token, but Vercel refuses to release sensitive values to anyone, including a token with account access. So I fetched that one from the Resend dashboard by hand, dropped it into a link like the others, and tapped one more approval before the assistant could use it.

The chat carried everything except the secrets, and that is what made the chat enough. There was never a moment where doing this safely would have forced me onto a laptop.

Two catches that mattered more than the speed

Two moments from the morning stayed with me more than the speed did.

First, the PR’s CI went red. My immediate assumption was that the migration broke something. The assistant checked before answering: main was already failing with the exact same error. That morning’s content publish had added a new storyline to the catalog without committing its rendered page, and my own editorial tests require every catalog item to have one. The red check came from main, not from the PR. It would have been easy to blame the check or quietly patch around it. Instead I got told it was my bug, unrelated to the migration, and that the same storyline was probably 404ing on the live site.

Second, after everything was green, it caught a problem in its own work. The wrangler config committed to the repo still had the routes block commented out, which would have made every future auto-deploy silently detach the custom domains. No alarm would have fired; the site would have just fallen off the domain on some random deploy weeks later. It committed the fix, waited for the deploy, and then re-ran the full verification against the live domain. The verification ran after the final change, not before it, which is what makes the rest of the speed believable.

What happens if I need to go back

Vercel is still standing. Its last deployment was never touched, and rolling back means re-adding two DNS records in Cloudflare, about a minute of work. What I turned off was the trigger: pushes to main no longer build on Vercel, so the two systems cannot drift apart. The fallback stays warm without either side fighting the other.

What actually made two hours possible

This is the part I want to be precise about, because the headline version of the story invites the wrong conclusion.

It was not typing speed. The migration worked because the system was already legible to an agent. The pipeline lived in a GitHub Action it could read. The data was plain JSON files, not a database with implicit state. The API was eleven small files with no framework magic. The repo documented its own conventions. There was almost nothing to reverse-engineer, so almost all the time went into doing and verifying instead of archaeology.

The second enabler was the credential flow above. Every secret moved through a secure link into a vault instead of through the conversation, so handing over access happened in the same thread, on the same device, as everything else. Remove that, and the same migration still happens, but not from a phone and not in two hours.

I wrote about making a site friendly to agents in February (Making Your Site Agent-Friendly), thinking about other people’s agents reading my content. I did not expect the payoff to arrive from my own side: the same clarity that lets an agent read a site lets one operate it.

The rest is what I wrote about in Shipping Is Cheap, Direction Is Expensive. My part this morning was a handful of decisions and a few secure taps. The work that used to fill a migration weekend - the porting, the config, the retries, the verification runs - collapsed into messages I read between other things. What did not collapse was the judgment: whether to move at all, when to flip, and what stays standing as the way back. I do not think it should.