Writing··9 min read
Moving letustype from Vercel to Cloudflare Workers
How letustype.com went from Next.js on Vercel to Cloudflare Workers with the OpenNext adapter, what broke on the way, and what the move actually costs.
letustype.com is a typing practice app built on Next.js, with Postgres behind it and Stripe in the middle because there is a paid tier. In June 2026 I moved it off Vercel and onto Cloudflare Workers with the OpenNext adapter. The hosting floor went from $20 a month to $5, the move took about a week of part-time attention, and AI agents did most of the mechanical work. This post walks through why I left a platform whose developer experience I genuinely like, what broke on the way, and where the money actually goes.
Why Vercel first
The repo's first commit is from April 22, 2026, and the app was on Vercel the same day. That is the whole pitch. You connect a repo, and deploys, HTTPS, and preview URLs simply exist. Every push got its own preview URL. And because Vercel builds Next.js itself, the fit is airtight: new framework features work on day one, and when something goes wrong the error message usually knows what you did. One of my earliest commits reads "run prisma generate on install so Vercel can find the client", and that was about as hard as deployment problems got for two months.
I was on Pro rather than the free Hobby plan for a boring reason: letustype charges money, and Vercel's fair-use guidelines restrict Hobby to personal, non-commercial use. Plenty of commercial side projects live on Hobby and nothing happens to them, but a paid product built on a tolerated exception is not a plan. Pro at $20 a month was the honest floor for a commercial app, and I paid it without resentment. The developer experience was worth something.
Why leave anyway
The $20 was never about usage. letustype came nowhere near Pro's included quotas of 1 TB transfer and 10 million edge requests. It was a floor, the price of showing up commercially, and it renewed whether I shipped that month or not.
What itched was paying that floor per seat, in US dollars, from Canada. Fifteen dollars a month of difference is $180 USD a year, and the exchange rate makes it land harder than the sticker suggests. Side projects survive by keeping their costs low, and I was noticing this one every month.
I looked hard at Netlify before Cloudflare. It is a long-time favourite, and my own site still runs happily there on a grandfathered plan. But accounts created since September 2025 land on credit-based pricing, where the free tier is 300 credits with a hard stop, a production deploy burns a flat 15 credits, and bandwidth burns 20 credits per gigabyte. I deploy constantly. A meter that ticks on every deploy is the wrong incentive for how I like to work, so Netlify was out, with some sadness.
The uncomfortable part is that Vercel is credit-shaped now too: the $20 platform fee includes a $20 monthly usage credit on top of the included quotas. Everyone's floor is quietly becoming a prepaid meter. Cloudflare's $5 is the floor that meters least at small scale, and the graph further down shows the more interesting question: where each platform's curve bends as traffic grows.
How the migration actually went
I did not want a leap-of-faith weekend. The migration ran in phases, and a
runtime conditional kept next dev and the Vercel deployment working the
entire time, so rollback stayed one config change away until the end.
Phases 0 and 1 landed on June 24: the OpenNext adapter build plus a batch of runtime fixes, verified locally under wrangler devCloudflare's CLI running your app on your machine inside workerd, the same runtime production uses.. Phase 2 was a soakRunning the system under real conditions for a while and watching for problems that only show up over time.: the full create, pay, grant lifecycle proven end to end against Stripe on workerdThe open-source runtime that powers Cloudflare Workers. It speaks web APIs, not Node.js, so Node-only code needs adapting. before real traffic touched it. Phase 3 was the cutover the same week. DNS and the .ca and www redirects moved to Cloudflare ahead of the switch, and Vercel was decommissioned on June 25. From the first prep commit on June 20 to that decommission, the migration came to roughly fifteen commits in one pull request, on a codebase that was 259 commits old at the time. The repo is private, so I will quote commit messages rather than link them.
The quirks (what OpenNext does not do for you)
OpenNext does the heavy conversion: it takes a Next.js build and produces a Worker. What it cannot do is make workerd be Node.js, and nearly every quirk below is that one fact wearing a different costume.
- Next 16 renamed middleware to
proxy.tsand runs it on the Node runtime. The OpenNext build rejects Node-runtime proxy files, so the code went back to the oldermiddleware.tsform on the Edge runtime. It is the same job under the older name, and only one of the two deploys to Workers. - NextAuth threw UntrustedHost the moment it left Vercel, because in
production it only auto-trusts hosts on platforms it recognizes. The
fix is one line,
trustHost: true. - Stripe webhook verification reaches for Node crypto by default. On
workerd it needed
constructEventAsyncwith the SubtleCrypto provider, plus Stripe's fetch-based HTTP client. - Prisma took the most work: two generated clients (workerd and Node) chosen at runtime, the HTTP driver adapter on workerd with a connection pool on Node, then later fixes for batch writes and for a bug that broke webhook de-duplicationletustype ignores a Stripe webhook it has already processed by catching the database's 'this row already exists' error. On workerd, Prisma's HTTP adapter did not translate that error code (P2002), so retried webhooks looked brand new..
- The
next/ogimage route loaded its font from disk withreadFileSync, which does not exist on workerd. The font is now base64-embedded. - Client IPs come from the
CF-Connecting-IPheader, not the socket. - The build baked environment secrets into the worker bundle. The deploy script now runs a sanitize step that strips them, and the migration playbook grew a secret-hygiene section. Whatever adapter you use, check this one; it is the scariest item on this list.
The best quirk surfaced about a month after cutover. The first deploy to contain
prerendered dynamic routes (typing-test topics and keyboard review pages,
both built with generateStaticParams) 404'd them in production while
every plain static route worked fine. The explanation took some digging: OpenNext
serves prerendered dynamic pages through an abstraction it calls the
incremental cache, the default implementation is in-memory, and a fresh
Workers isolateA lightweight sandbox Cloudflare spins up to run your Worker. Many run at once, they start in milliseconds, and each begins with its own empty memory.
starts with that memory empty. Production runs many
short-lived isolates, so as far as production was concerned, those pages
did not exist. Local dev, one warm long-lived process, never showed a
thing. The fix is one config file: tell OpenNext to read
prerendered pages from the static assets bundle it already shipped.
// open-next.config.ts: the whole fix
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: staticAssetsIncrementalCache,
});unstable_cache had the same disease. Per-isolate memory that is almost
always empty means it silently does approximately nothing on Workers, so
it got replaced with a plain in-process cache with a short expiry, one
that at least admits what it is.
The agentic part, openly
I want to be plain about this, because a lot of writing in this genre gets cagey exactly here: AI agents wrote most of the mechanical code in this migration. The repo runs a spec-driven workflow (numbered feature specs, agent instructions, review gates), and the migration was executed inside it, with Claude Code doing the bulk of the reading and writing.
At times I asked GPT 5.5, through Codex, to read over what had been written. It was nothing ceremonial, just a second opinion from a different model family, one that reads with different blind spots.
The real win was brain space. I stayed in the loop on why every change was being made, without having to hold workerd API details, Prisma adapter behaviour, and OpenNext documentation in my head at the same time.
There is also the part Vercel normally does for you. Workers Builds CI (push to main, build, deploy) took real setup and was working by June 25. With Claude Code in the loop that setup was cheap. A couple of years ago, doing all of this by hand, down to reading adapter source to work out why prerendered pages 404, the effort might have been the reason to stay put. Verification stayed old-fashioned: a workerd smoke testA fast automated check that a deployed app's critical paths respond at all, run right after a deploy., the soak phases proving Stripe end to end, and CI gates added mid-migration.
What the money buys (rates verified July 2026)
- Vercel Pro at $20: one deploying seat, a $20 monthly usage credit, 1 TB Fast Data Transfer and 10M edge requests included; past that, US rates run about $0.15 per GB and $2.00 per additional million edge requests.
- Workers Paid at $5: account-level and shared by every Worker on the account, with 10M requests and 30M CPU-milliseconds a month included, then $0.30 per extra million requests and $0.02 per extra million CPU-ms. Requests to static assets are free and unlimited, and there is no egress or bandwidth charge at all.
- The one Workers meter that grows with success is logging: Workers Logs includes 20M events a month, then $0.60 per million. letustype currently records every request (sampling rate 1.0), which costs nothing at its traffic and makes debugging pleasant; the config comment already plans to sample down if volume ever makes the meter move.
- The multi-project kicker: concertprep.app already runs on Workers, so the $5 was effectively already being paid. letustype's marginal hosting cost after migration is roughly zero, and the next side project's will be too. Vercel Pro also covers unlimited projects, but at four times the floor, priced per seat rather than per account.
Pros and cons
The sections above condense to two tables.
| Pros | Cons |
|---|---|
|
|
| Pros | Cons |
|---|---|
|
|
Who should stay on Vercel
Three groups should stay, honestly. The first is teams whose review culture runs on Vercel's collaboration layer. Workers gives every new version an automatic preview URL, so previews alone are not the moat. The layer on top is: comments on previews, protected share links, and a dashboard a non-engineer can read. Apps that lean hard on ISR or image optimization should probably stay too; letustype used neither, and that is a big part of why this migration came out clean. And anyone whose time is currently worth more than the difference should stay, because the $15 a month was never really the point. The point is the ceiling, and if your app makes real money, the ceiling is a problem you will be glad to have.
Verdict
I would make the same move again for this class of app: a commercial Next.js project with modest traffic, no ISR, and an owner comfortable holding a little more of the pipeline. What I watch now is a short list, OpenNext adapter releases and the logs meter. And the bar for going back is simple: Cloudflare would have to end Worker support for Next.js, or the bill would have to cross $20 a month.
