Last updated on

Planning the Move to Production


I promised a follow-up on the content model — the collections, the Local API, the rich text setup — and that's still coming. Before it does, though, this site needs to actually go live, and getting there turned out to involve more decisions than I expected for what's, on paper, a personal blog. This post is about that plan. I want to be upfront about what it is: a plan, written before executing it, not a report on how the deploy went. If something in here turns out to be wrong once I actually run it, I'll say so in the follow-up rather than quietly editing this post to look like I got it right the first time.

The database decision that turned out not to be a decision

The obvious first question was where the production database lives. I went in assuming "Vercel Postgres vs. Neon directly" was a real cost tradeoff to work through. It isn't, and finding that out was itself useful.


Vercel Postgres, as its own product, doesn't exist anymore — Vercel migrated every Vercel Postgres database to Neon in December 2024, and what's offered today is a Neon integration through Vercel's Marketplace. So "Vercel Postgres vs. Neon" isn't a choice between two databases with different pricing. It's the same database, the same pricing tiers, with exactly one real difference: going through the Marketplace bills through Vercel and wires the connection string into my project automatically; going direct to Neon means a separate account, a separate invoice, and copying the connection string over myself.


For a solo project, the decoupling that direct Neon buys you only matters if you're actually planning to leave Vercel at some point. I'm not, so I picked the Marketplace route — one less account to think about. The lesson I'd actually keep from this isn't the choice itself, it's that the question I started with ("which is cheaper") was the wrong question, and I only found that out by looking instead of assuming.

Two migrations, not one

The site currently runs on a local Postgres database with real content in it — not placeholder text, actual posts. Moving that to production means two migrations that are easy to collapse into one in your head, and shouldn't be:


The database migration is the content: posts, pages, site settings, all the structured rows Payload manages. That part is mechanical — dump the local database, restore it into the new one. Because it's a straight database clone rather than replaying Payload's schema migrations, it also sidesteps a gap I've been carrying since the last round of changes: there's no migrations directory in this project yet, because local development has been running on Payload's automatic schema push, which production disables. Cloning the database directly means that gap doesn't block this deploy. It doesn't close the gap, either — the next time the schema changes after the site is live, I'll need real migrations, not push. That's a problem for future me, noted here so it doesn't quietly become a surprise.


The media migration is the actual uploaded files — images sitting on local disk. Those aren't in the database at all; the database just has rows that reference filenames. This is the migration that's easy to forget, because the site will build, deploy, and look completely fine until someone loads a page with an image on it.

Why the order matters more than either step alone

Right now, uploaded media defaults to local disk storage, which doesn't survive Vercel's ephemeral filesystem. If I migrate the database before wiring in real storage, every image reference in production points at a file that doesn't exist anywhere production can reach. The fix isn't complicated — add a storage plugin, point it at Vercel Blob — but it has to happen before the data migration, not after, or the first real visitor to the site sees a page full of broken images while I figure out why.


This is the part of infrastructure work that doesn't photograph well: no clever architecture, just getting the sequence right so that a boring thing doesn't break in a completely predictable way. I'd rather write about that plainly than skip to the parts that sound more impressive.

What I'm choosing not to solve right now

A few things are staying out of scope for this first deploy, on purpose: no automatic rebuild when content is published in the admin UI (that needs a deploy hook, which needs a real deployment URL to point at — chicken, egg), and no real migrations directory, for the reason above. Both are real gaps. Neither is a reason to delay getting the site live, and pretending otherwise would just be a way of never shipping.

What's next

Once this actually happens, I'll write the follow-up — what in this plan held up, what didn't, and what broke that I didn't anticipate here. Something usually does. After that, back to the content model post I owe from last time.