
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.
| Topic | Primary source |
|---|---|
| Convex import and export | Convex import and export |
Convex file storage (_storage) | Convex file storage |
| Neon connection and branching | Connect to Neon |
| Drizzle migrations | Drizzle migrations |
Last updated: May 31, 2026. Verify tool behavior and migration commands against the official docs before you ship.
A week ago I wrote up the Convex to Neon migration on a single app, dd-clipper, as a practical notes post. Five tables, five PRs, a planning doc, and a list of lessons we paid for in time.
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.
For the implementation path around this, pair it with How to Build Full-Stack TypeScript Apps With AI in 2026 and The Next.js AI App Stack for 2026; those guides connect the idea to a shippable TypeScript stack.
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.
From the archive
Apr 28, 2026 • 9 min read
Apr 28, 2026 • 9 min read
Apr 28, 2026 • 6 min read
Apr 28, 2026 • 8 min read
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 rest of the migration notes in this series cover the trade-offs in more detail.
Export your Convex data, list the tables you actually have, then walk your codebase callsites for useQuery, useMutation, and any file storage usage. The migration plan is only as good as the inventory.
Not if you can avoid it. Treat file storage as a separate project: inventory files, decide on the destination (S3, R2, etc.), backfill, and only then delete Convex storage. The playbook works best when file storage is a second pass.
drizzle-kit push safe for production?It can be, but treat it as a workflow choice. For early stages, pushing schema can be fast. For mature systems, generate and review SQL migrations so schema changes are explicit and code-reviewed.
There is no universal replacement. For many CRUD apps, polling and revalidation are enough. If your app needs real-time collaboration, the migration includes picking a real-time layer, not just swapping a database.
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.
Read next
Neon's branching model, serverless driver, and scale-to-zero autoscaling make it one of the most practical Postgres hosts for teams building AI agents and preview-heavy apps. Here is what you need to know before committing.
9 min readHow we ported 38 apps off Replit and onto Coolify in a single day, using parallel Claude Code subagents, gh, and neonctl. The honest stats: stubs, monorepos, false-empties, and ~120 PRs.
8 min readFable 5 is mostly a drop-in replacement for Opus 4.8, but 'mostly' is doing real work in that sentence. Here's every breaking change, what to delete from your code, and the prompt audit you should run before flipping the model ID.
9 min readTechnical 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 ToolTypeScript ORM with a schema-first workflow. Prisma Client gives full type safety; Prisma Migrate handles migrations. Wo...
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 ToolNeon's branching model, serverless driver, and scale-to-zero autoscaling make it one of the most practical Postgres host...

How we ported 38 apps off Replit and onto Coolify in a single day, using parallel Claude Code subagents, gh, and neonctl...

agentfs is filesystem-shaped storage for AI agents. Postgres-backed on Neon, no cold starts, no exec by design. Pay-only...
deepseek-chat is deprecated and disappears July 24, 2026 - here is how to migrate to V4 Flash or Pro, with verified pric...
Pricing deadlines, infrastructure funding, a banking prompt injection case, and a 4x speed breakthrough - June 10 was on...
Windsurf is now Devin Desktop, owned by Cognition after a turbulent 2025 acquisition saga. If the ownership shuffle has...

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