What Actually Happened Deploying This Site
Last post I laid out a plan for getting this site onto Vercel and migrating real content into production, and I said something deliberately at the end of it: something usually goes wrong that the plan didn't predict, and I'd write the follow-up once I knew what. Here it is. The plan mostly held up in outline — Neon via Vercel, a storage plugin for media, a database clone instead of migrations — but almost nothing about the execution went the way I expected, and three moments in particular are worth telling in full rather than folded into a changelog.
The order I planned was backwards
The original plan assumed a specific sequence: deploy the app first, so the storage plugin's code and a real Blob token both exist in production, then migrate the database afterward. The reasoning was sound for the media side of things. It was wrong for the database, and I didn't find out until a build failed with relation "posts" does not exist.
The part I hadn't accounted for: this site prerenders every blog post at build time, not request time. generateStaticParams() queries Postgres directly during next build to know which slugs exist, which means the build itself needs a populated, schema-complete database before it can finish — not just before a visitor loads a page. My plan had the database migration happening after a successful deploy. For a site with build-time static generation, that dependency runs the other way: the database has to exist first, or there's no successful build to have a "before" for.
This is the kind of mistake that's obvious in hindsight and easy to miss going in, because "deploy the app, then migrate the data" sounds like a reasonable default until you actually know how your specific framework generates pages. I'd rather say plainly that I got this backwards than describe the final working order as if it were the plan all along.
Turning on the feature that was supposed to help
Vercel caps server-side upload requests at 4.5MB, and Payload's Vercel Blob plugin has a clientUploads option specifically to route around that limit by uploading straight from the browser instead. It seemed like the correct call, so I turned it on.
It broke uploads immediately, and the failure disguised itself well: the browser reported a CORS error, which sent me looking at allowed origins and store configuration for longer than it should have. The real cause was underneath that — the plugin's client-upload path has open, tracked bugs sending malformed requests to Vercel's Blob API, and Vercel's rejection of that malformed request just happened to omit the CORS headers a normal response would have, which is what made the browser report the wrong problem. Once I found the actual GitHub issues describing it, the fix was simple: turn it back off. Server-side uploads, capped at 4.5MB, are the plugin's tested default. The client-upload path is a real feature with a real reason to want it, and it just doesn't work reliably yet on the version this project is pinned to.
The judgment call here wasn't technical, it was about when to stop. I could have kept digging into a third-party plugin's internals to make an optional performance feature work. Reverting and accepting a real constraint (keep hero images under 4.5MB) was the faster, more honest trade, and I think it's usually the right instinct when the thing you're fighting is someone else's unresolved bug rather than your own code.
The bug that wasn't a bug
The last one is the strangest, and it happened after everything above was actually fixed. DNS was correctly pointing at Vercel. Vercel's own domain check confirmed it. A fresh DNS lookup from a completely different network confirmed it again. And every time I visited the site in my regular browser, it redirected to my GitHub profile.
This domain used to point at a GitHub Pages site before any of this project existed, and that old site had, at some point, registered a Service Worker in my browser. Service Workers are deliberately persistent — built to keep working offline, which means built to keep intercepting requests to that origin regardless of what the actual server is doing now. Mine had cached the old redirect-to-GitHub behavior and kept replaying it locally, without touching the network at all, which is exactly why DNS, hosting, and Vercel's own dashboard all correctly reported that nothing was wrong: from the server's perspective, nothing was. The bug was sitting in my own browser's storage, several steps removed from anything I was looking at.
I found it almost by accident — opening DevTools happened to change how the browser handled the Service Worker on that load, and the real site suddenly rendered. Unregistering the Service Worker and clearing the site's storage fixed it permanently. It's also, worth saying plainly, not a bug that affects anyone else: a Service Worker only exists on a device that already visited the old version of the site, so a new visitor was never going to hit this.
Key Takeaways
- Validate dependencies early: Understand how your framework generates pages (build-time vs. request-time) before planning deployment sequences.
- Know when to revert: If a third-party plugin is failing due to known bugs, accepting reasonable constraints (like file size limits) is often more efficient than debugging external code.
- Rule out the local environment: When everything else looks correct, strange bugs (like persistent Service Workers) might be local to your own browser, not the live site.
The rest of it
Underneath those three, there was a steady stream of smaller friction that's less interesting individually but adds up to most of the actual time spent: a Next.js build cache serving stale code after a config change that should have taken effect immediately, a Blob store created as private when the app expected public access, a malformed database connection string from a bad copy-paste, and an HTML comment that had somehow ended up inside a .env file and broken a shell script's parsing of it. None of these are interesting on their own. Together, they're an honest picture of what deployment work mostly is — not architecture, mostly just careful attention to configuration details that are individually small and collectively where all the time goes.
That's the actual gap between the plan and what happened: the plan was right about the shape of the work and wrong, repeatedly, about the details underneath it. I don't think that's a failure of planning so much as a fact about this kind of work — you can lay out the architecture correctly and still spend most of your time on things a plan can't anticipate, because they're specific to the exact versions, exact providers, and exact history of the thing you're actually deploying, not the general case.
There's still a post I owe from before any of this deploy detour started: what the actual content model looks like — the collections, how Payload's Local API and rich text ended up wired into the frontend, the parts of the admin setup that are easier to show than to describe. That one's next, for real this time.