DEV Community

Garoro0920
Garoro0920

Posted on

I built an AI agent that migrates Next.js Pages Router to App Router

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 / getServerSidePropsasync Server Components
  • getStaticPathsgenerateStaticParams
  • next/routernext/navigation (useRouter / usePathname / useSearchParams)
  • next/head → the Metadata API (export const metadata)
  • pages/_app.tsx + pages/_document.tsxapp/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
Enter fullscreen mode Exit fullscreen mode

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)