
TL;DR
We ran the same Convex to Neon migration on four apps in a week. Here is what stayed identical, what differed per app, and the real speed-up by app two.
A week ago I wrote up the Convex to Neon migration on a single app, dd-clipper, as a war story. Five tables, five PRs, a planning doc, and a list of lessons we paid for in time. That post lives at migrating Convex to Neon and Drizzle.
Tonight three more apps shipped the same migration in one sitting. dd-skills-marketplace, dd-hooks-directory, dd-mcp-directory. A fourth, adcraft-ai, is in flight. One app is an anecdote. Four apps is a pattern.
This post is the sequel. It pulls the moves that worked on dd-clipper out of the war story and writes them down as a reusable playbook. What stayed identical across every app. What was actually app-specific. And the part everyone wants the number for, how much faster the second migration was than the first.
The first migration is a project. You discover the shape as you go. You write a planning doc, you find out which guarantees you were quietly relying on, you get burned by a credit deduction race, you realize file storage is a separate problem.
The second migration is a checklist. You already know what useQuery reactivity replaces with. You already know UPDATE ... RETURNING is the credit pattern. You already know the deprecated convex/ directory stays in the repo for a release. You already know what the README cutover note looks like.
The third and fourth are template work. Lift the schema layout, point an agent at the table list, run the playbook, ship. That is the speed-up. Not a clever trick, just the normal compounding you get when you stop solving the same problems twice.
For portfolio context, see the DD apps overview and the stack comparison page. For the tooling story behind running four migrations in one night, the overnight agents post covers how the agent fan-out actually worked.
Every one of the four migrations followed the same seven steps in the same order. This is the version that is now copy-pasted into a checklist file at the top of each migration PR.
useQuery, useMutation, and _storage. This becomes the planning doc. Half a page is fine. The point is that nothing surprises you in step five.pnpm db:push against a fresh Neon branch and see the empty tables come up.db/ module per table with the same shape the old convex/<table>.ts exported. Import lazily so the dev server does not crash on missing env vars in unrelated routes. This is the part most people skip, and it is the reason migrations stall in review.convex/<table>.ts does not get deleted, it gets a // @deprecated banner and an empty body that throws a clear error if anyone still imports it. Generated client types stay valid, downstream SDKs do not break.UPDATE ... RETURNING, INSERT ... ON CONFLICT, SELECT ... FOR UPDATE inside a transaction. No SELECT-then-UPDATE patterns survive review.useQuery. Polling interval, optimistic updates, server-sent events, or "we accept staleness." This is a one paragraph decision. Do not skip it and do not defer it.convex/ is deprecated and removed on the next release. Without this, the next person who clones the repo runs npx convex dev and is confused for an hour.That is the whole playbook. Seven steps, no novelty, all boring on purpose.
Get the weekly deep dive
Tutorials on Claude Code, AI agents, and dev tools - delivered free every week.
Four things were exactly the same in every migration. Word for word in some cases. These are the parts you can lift verbatim.
The lazy proxy pattern. Every db/ module wraps Drizzle calls in a function that constructs the client on first call, not at import time. Without this, any route that imports a sibling module gets an "env var missing" crash in dev when you have not yet wired up DATABASE_URL for that route. The pattern is three lines and it shipped unchanged in dd-clipper, dd-skills-marketplace, dd-hooks-directory, and dd-mcp-directory.
The deprecated convex/ directory. None of the four migrations deleted Convex on the way out. Each one kept the directory, marked every file with a deprecation banner, and stubbed the exports to throw a readable error. This protects the generated Convex client types that downstream tooling sometimes still references during a release window. Deletion happens in a follow-up PR after one full deploy cycle.
The atomic UPDATE ... RETURNING for any counter. Saves counts, install counts, ratings totals, credit balances. Every app had at least one of these. Every app got the same single-statement pattern. This is the lesson from dd-clipper that paid for itself three more times.
The README cutover note. Same three bullet points each time. Where the database URL goes, what the migration command is, and what is deprecated. It is the smallest part of the playbook and the highest hit rate on developer confusion when it is missing.
If you are running this migration on your own app, those four are the parts you do not have to think about. They are not load-bearing decisions, they are just the right answer.
Three things were genuinely app-specific. This is where the playbook stopped being a copy-paste and started needing judgement.
Table count and shape. dd-clipper had five tables and a real domain model with credits, usage logs, and clips. The directory trio (skills, hooks, mcp) had four small tables each with the same shape, basically items + saves + installs + ratings. adcraft-ai has seven and a more complex one, including users, generations, canvas elements, and brand kits. The playbook scales to all three sizes, but the work per table is not constant. Domain tables with business logic take longer than directory join tables, and the second migration did not magically make domain logic faster.
Reactivity tolerance. The directory trio did not need useQuery reactivity at all. A user saves a hook, the page revalidates on next nav, nobody notices. dd-clipper needed reactivity for the clip library and we made the call to accept polling. adcraft-ai has a canvas with multiple elements being edited and that is genuinely a place where reactivity matters, so the migration there has to pick a real-time layer up front, not punt. The playbook step says "make the decision," not "the decision is the same." Across four apps the decision was different three times.
File storage. dd-clipper had clip blobs in Convex _storage. The directory trio had no file storage at all, which made their migrations meaningfully smaller. adcraft-ai has generated images and brand kit assets, which is its own project on top of the table migration. The playbook explicitly separates these. If your app has files, expect the migration to be two projects, not one. If it does not, you just got a free speed-up.
Here is the part everyone wants. I am going to be direct because the rounded version is misleading.
dd-clipper took most of a week of evenings. Five PRs, a planning doc, a real war story. Some of that was migration work. A lot of it was figuring out the playbook by hitting walls.
The directory trio shipped tonight in one sitting. Not parallelized in the agent-team sense, but each migration was under an hour of focused work once the playbook was in hand and an agent was running the table-by-table scaffolding. Three apps, one evening, three merged PRs. The PRs are small because the apps are small, and the apps are small because they are directories, but the structural reason it was fast is that none of the seven steps required new thinking.
The honest framing is this. The first app paid for the playbook. The second app validated it. The third and fourth apps were the payoff. If you are looking at a portfolio of similar apps and trying to decide whether to migrate the cheap one first or the expensive one first, do the expensive one first. The playbook you build will pay for itself across everything else.
adcraft-ai is in flight as I write this. Seven tables, real reactivity needs, file storage to plan around. I expect it to land somewhere between dd-clipper and the directory trio in time, probably closer to dd-clipper because the domain logic is real. The flagship site, developers-digest-site, has seventeen tables and is on deck after that. That one is its own multi-PR series.
The playbook is right when you have a portfolio of small to medium apps that all share Convex as a dependency, you already run Postgres elsewhere, and the split-stack tax across apps is real. The compounding only happens if you have more than one migration to do.
The playbook is right when your reactivity needs are tractable. Polling works for directories. Polling plus optimistic updates works for most CRUD. If your app genuinely needs collaborative editing or live presence, the migration is still doable, but the reactivity decision in step six is now a real architecture project, not a paragraph.
The playbook is wrong when your app is one app and it works fine on Convex. The first migration cost is real. You only get the speed-up if there is a second app to apply it to. If you have one Convex app and it ships, leave it.
The playbook is wrong when most of your data is files. If your Convex usage is mostly _storage with a thin metadata table on top, the migration is a file storage project with a side of SQL, and the seven steps above are the wrong frame.
If your app is on Convex, working, and you are trying to decide whether any of this applies to you, the tools comparison page has the side-by-side, and the original migration post has the war story version with code samples for the parts that took us the longest the first time.
Four apps in, the migration is no longer interesting. That is the goal. Boring is the point. The next app is just the playbook again.
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.
Type-safe SQL builder and ORM for TypeScript. Zero runtime overhead, honest schema migrations, bring-your-own-DB.
View ToolReactive backend - database, server functions, real-time sync, cron jobs, file storage. All TypeScript. This site's ba...
View ToolFactory AI's terminal coding agent. Runs Anthropic and OpenAI models in one subscription. Handles full tasks end-to-end...
View Tool
A war-story walkthrough of moving a 5-table app from Convex to Neon with Drizzle, one PR at a time, with the trade-offs...

agentfs is filesystem-shaped storage for AI agents. Postgres-backed on Neon, no cold starts, no exec by design. Pay-only...

Convex and Supabase both work for AI-powered apps. Here is when to use each, based on building production apps with both...

New tutorials, open-source projects, and deep dives on coding agents - delivered weekly.