Most Next.js teams have a Pages Router → App Router migration sitting in their backlog. It's mechanical but careful work, and it keeps getting deprioritized. I built migrate-bot to automate it end-to-end, and this post is about how it works under the hood.
What the migration actually involves
App Router isn't just "move files to a new folder". The semantic changes:
-
getStaticProps/getServerSideProps→asyncServer Components -
getStaticPaths→generateStaticParams -
next/router→next/navigation(useRouter/usePathname/useSearchParams) -
next/head→ the Metadata API (export const metadata) -
pages/_app.tsx+pages/_document.tsx→app/layout.tsx - API routes:
pages/api/x.ts(single handler) →app/api/x/route.ts(export async function GET/POST/...) - The
pages/→app/restructure, including[slug]dynamic routes
Architecture
The whole thing runs serverless + ephemeral:
- Cloudflare Workers host the API and landing site
- Cloudflare D1 (SQLite) stores jobs, orders, installations
- Cloudflare Queues decouple the webhook handler from the migration pipeline (producer/consumer with an idempotency check so retries don't double-charge)
- Fly.io Machines run the actual migration: one ephemeral VM per job, destroyed at job end (no code retention)
- Anthropic Claude does the per-file transforms (Sonnet by default, Opus as a one-shot retry fallback)
- Stripe for payment, Resend for transactional email
The pipeline as a state machine
queued → analyzing → planning → migrating → verifying → pr_ready
↓ (on failure)
aborted_blocker → refunding → refunded
Each transition is persisted to D1 so every job is observable, and a mid-pipeline crash transitions to aborted_blocker → automatic refund rather than leaving the customer charged for nothing.
A subtle bug: _document.tsx
Early on, every repo with a pages/_document.tsx failed. The planner maps both _app.tsx and _document.tsx to the same target (app/layout.tsx), and my migrate step recorded the second one as a "failed task" when it detected the target was already written. The pipeline treated any failed task as fatal — so the whole migration aborted.
The fix was to distinguish intentional skips (planned target collisions) from real failures (the LLM aborting or erroring). I added a skippedTaskIds field separate from failedTaskIds, and surfaced the skip in the PR description so the customer knows to manually merge any custom <Html> attributes from their _document.tsx.
Trust model
Since this rewrites real production code, the trust mechanisms matter:
- The output is always a draft PR — your CI runs first, you review, you merge. Nothing is auto-merged.
-
14-day full refund if the verify step (typecheck +
next build) can't pass for reasons attributable to the service. - Code is processed only on an isolated VM and is never used to train any model.
Try it / feedback
It's live at migrate-bot.dev. Pricing is per-repo by file count ($99 / $249 / $499). I'd genuinely value feedback on the approach — especially the pricing shape and the draft-PR + refund trust model.
(Currently not offered to EEA/UK/Switzerland residents due to GDPR territorial scope; revisiting post-launch.)
Top comments (0)