Changing Everything at Once

Seven migrations, one switchover, zero data loss

@vieko

Sometime last year, I told James Mikrut, Payload's creator, that I was going to steal his footer treatment right after migrating to the latest beta for his CMS[1].

I'd been planning a revamp of Mothership, Devolver's flagship website, for some time. This iteration was built for a catalog of 33 games. On the road to publishing 100 more games, the web had evolved--and so had our needs.

Games deserved better showcasing and more chances for discovery. Users deserved a better experience. We needed to move faster. Upgrading Payload was only part of the puzzle.

Changing Everything

Mothership powers landing pages, a games catalog, news articles, company pages, and community features for Devolver Digital and related properties. That's a far cry from an e-commerce platform with millions of SKUs, where you follow Vercel's playbook[2] instead of improvising. Our scale meant I could build incrementally in development while keeping the old stack as a fallback, then execute a coordinated switchover. No dual systems running in production.


RepositoriesTurborepo

Multiple repositories consolidated into one high-performance, multi-package build system

Page RouterApp Router

Fetch data directly in components and send less JavaScript to the client

JavaScriptTypeScript

End-to-end type safety from server to client without custom serialization

Payload 1.0Payload 3.0

CMS is built by the same Turborepo and is deployed serverless

SlateLexical

Payload's new default editor, with modern architecture and better performance

MongoDBPostgres

Relational data model fits the CMS structure and integrates with App Router patterns

CloudinaryVercel Blob

Consolidated on Vercel Cloud, and integrates with Next.js image optimization


I migrated the frontend to TypeScript and App Router while keeping the existing CMS, database, and assets. I polished the UI and tightened branding where quick wins made sense. Once the frontend reached parity, I cataloged all content and asset references, then migrated to Payload 3.0 and brought assets over to Vercel Blob. Only then did I have fun redesigning landing pages and structures, hiding a handful of Volvy easter eggs here and there.

Volvy easter egg

Building Tools

I built analysis tools to understand what I was working with. An export scanner cataloged 2,184 records across 10 collections. A rich text analyzer mapped 7,372 Slate nodes across 240 documents--every heading, link, custom YouTube embed, and text format. An asset scanner checked 1,701 media files for actual usage.

Migration scripts were designed to pick up where they left off--state files tracked every ID mapping, warnings were captured for review, and dry runs tested changes without risk. When the asset migration hit a Cloudinary rate limit, I stopped it, waited, and resumed. No data lost, no restart from zero.

Finally, verification tools validated everything end-to-end. The final report confirmed 467 content records migrated, 592 assets moved, zero broken relationships.

Orphaned Assets

The asset scanner found orphaned files everywhere--abandoned uploads from failed drafts, deleted content, experiments that never shipped. Meanwhile, 599 Cloudinary URLs were hardcoded directly into rich text fields. We pasted image URLs because creating a proper media relationship took more clicks.

When the proper workflow is harder than the workaround, people choose the workaround. The migration scope dropped from 1,701 files to 629--a 63% reduction before a single file moved.

What Got Left Behind

The migration exposed years of accumulated cruft. Prototype collections that were never removed. Features that were built, tested, then abandoned. Fifty-five merch items with no descriptions. Video fields in games that were never implemented. User types that no longer had a purpose.

Technical debt doesn't fix itself (at least not yet) and without a forcing function, it becomes permanent. We cut what didn't need to move and improved what was working.

Launch Day

Quite boring with little fanfare and just a small issue: preview links returned 401s. Cross-domain cookie authentication meant the CMS on cms.devolverdigital.com couldn't share auth cookies with devolverdigital.com. Classic "works on localhost" scenario.

The fix? An intermediate redirect that generates a token, passes it via query string, and validates it on the website side. The entire debugging, implementation, and deployment cycle took minutes on Vercel.


Mothership landing page
Mothership landing page
Mothership games catalog page
Mothership games catalog page

Seven migrations. Zero data loss. Analysis tools cut the scope by 63%. Resumable scripts made failures recoverable. Real data testing caught what unit tests missed. A launch-day auth bug still showed up--and was fixed in minutes.

As for Payload's beta and that gorgeous footer treatment? I never got around to either. I went straight to 3.0 stable when it shipped and left a // TODO in the code encouraging whoever finds it to give the footer a shot. Seven migrations taught me when to change everything--and when to leave something for later.


Built from contemplating commits and session state files, with Claude Code as my pair migration partner.


[1] The actual conversation with James: "If you're feeling adventurous, you could go straight to 3.0 beta right now. But if you'd prefer to wait until we release 3.0 docs, then maybe wait a few weeks until 3.0 stable and then go straight to 3.0! In either case I wouldn't worry about going to 2.0 right away."

[2] Incremental migrations involve gradually transitioning to a new system where both old and new systems operate in parallel, moving features or users in phases rather than all at once. Read more in Malte's article.