afterbuild/ops
Resource

Migrate from Bolt.new to Next.js — step by step.

Bolt.new gets you to a working app fast. But at some point you need to own the infrastructure, control the deploy, and stop paying per token. Here's the full migration path.

By Hyder ShahFounder · Afterbuild LabsLast updated 2026-04-15

Bolt.new is the fastest way we've seen to go from an idea to a working prototype. You describe the app, StackBlitz spins up a WebContainer, and within minutes you have something that runs in the browser. For validation work, pitch decks, and internal tools, it's hard to beat. But there is a ceiling, and founders hit it in a predictable way: the app starts feeling fragile, the token bill climbs, a feature the AI can't finish stalls for a week, and a prospective enterprise customer asks where the source code lives. That's the moment migration becomes the right call.

This guide walks through the full migration path from a Bolt.new app running in StackBlitz to a self-hosted Next.js codebase on Vercel. It covers code export, the Vite-to-Next.js conversion (the biggest single step), database wiring for Supabase and Neon, auth migration, Stripe webhook re-routing, and the first production deploy. We assume you have a working Bolt app and basic familiarity with Git, the command line, and your preferred code editor.

When you should migrate:your app has real users or paying customers, you're hitting the fix loop where every change breaks something else, your monthly Bolt token bill is crossing $200, the AI has stalled on a feature for multiple days, or an investor or enterprise customer is asking about your tech stack. Any of those are legitimate migration triggers.

When you should not migrate:you're still validating the idea, you have fewer than ten users, the app works and nobody is asking hard questions about hosting or source code, or you haven't yet taken money from customers. Migrating prematurely adds engineering overhead to a product that hasn't earned that overhead yet. Stay in Bolt until the product has real signal.

1. What you're actually migrating

Bolt.new apps are not Next.js apps. Under the hood, Bolt generates a React application — almost always a Vite-based React project — and runs it in a StackBlitz WebContainer. The WebContainer is a browser-hosted Node runtime that makes it feel like a local dev environment, but the project itself is just a React + Vite codebase with whatever packages the AI installed. The migration is less about rewriting the app and more about swapping the parts that are Bolt-specific for parts you own.

Concretely, here's what changes during a migration:

What does not change: the business logic of your React components. A <PricingTable /> component in Bolt is the same component in Next.js — the JSX, the hooks, the API calls, and the styling all transfer without modification. The file it lives in moves, and you may need to add a "use client" directive at the top, but the component itself is portable. The database (Supabase, Neon, PlanetScale) usually stays where it is and just needs an env-var rename. External APIs you call don't care where your code runs.

2. Export your Bolt.new code

Bolt stores code in StackBlitz, which is both the runtime and the file system. To migrate, you need the code out of StackBlitz and into a Git repository you control. Bolt gives you two ways to do this. The preferred path is the GitHub integration; the fallback is a ZIP download.

GitHub export (preferred)

In the Bolt UI, look for the GitHub icon or the Export menu. Connect your GitHub account, authorise Bolt to create repositories, and pick a name. Bolt creates a new repo with the full contents of the StackBlitz project and pushes an initial commit. Clone the repo locally:

git clone https://github.com/YOUR_USER/YOUR_REPO.git
cd YOUR_REPO
npm install
npm run dev

If npm run devworks locally, you have a valid Vite React project and you're ready to continue. If it doesn't run, debug the local setup first — a migration on top of a broken local build will compound problems.

ZIP download (fallback)

If GitHub export isn't available or fails, download the project as a ZIP, extract it, cd into the directory, initialise a Git repo, and push it to GitHub manually. Either way, the result is the same: a Git repo on your account with the full Bolt project.

Verify the structure

Open package.json. Confirm it lists vite and react as dependencies. Note the entry point (usually src/main.tsx or src/main.jsx), the router (look for react-router-dom), and any auth/payments packages (@supabase/supabase-js, @stripe/stripe-js, @clerk/clerk-react). Also look for StackBlitz-specific imports: anything under @webcontainer/*is a runtime that doesn't exist outside StackBlitz, and it will need to be removed or replaced. These are rare but they do show up in apps that use the virtual file system directly.

3. Convert Vite to Next.js App Router

This is the biggest single step of the migration and the one most likely to produce bugs. The cleanest approach is to create a fresh Next.js project and move code into it, rather than trying to modify the Vite config to become Next.js. Start with:

npx create-next-app@latest my-app --typescript --app --tailwind --src-dir
cd my-app

That gives you a canonical Next.js App Router project with TypeScript, Tailwind, and an src/directory. Now copy files from the Bolt project in stages rather than all at once. Here's the order we use:

  1. Dependencies. Open the Bolt package.json and install everything the Bolt app depends on (excluding vite, @vitejs/plugin-react, and react-router-dom, which Next.js replaces).
  2. Shared utilities and types. Copy src/lib/, src/utils/, and src/types/ from Bolt into the new project. These are pure TypeScript and should move without modification.
  3. Components. Copy src/components/. Most components will work. Any component that uses useState, useEffect, useRouter, window, or localStorage needs "use client" at the top of the file — these are client-only components in the App Router model.
  4. Routes.For each route in the Bolt app's React Router config, create a matching folder under src/app/. A route like /dashboard/settings becomes src/app/dashboard/settings/page.tsx. The component that React Router rendered becomes the default export of page.tsx.
  5. Layouts. If the Bolt app has a shared layout (nav + footer wrapped around all pages), that becomes src/app/layout.tsx in Next.js. Nested layouts become layout.tsx files in subdirectories.
  6. Styling. If the Bolt app uses Tailwind (most do), copy the tailwind.config.js content (colors, fonts, extensions) into the Next.js Tailwind config. Copy any global CSS into src/app/globals.css.

Replace react-router-dom with next/link and useRouter

Every <Link to="/foo" /> from react-router-dom becomes <Link href="/foo" /> from next/link. Every useNavigate() becomes useRouter() from next/navigation (with router.push() replacing navigate()). Every useParams() and useSearchParams() import changes source from react-router-dom to next/navigation. The API surface is similar but not identical — test each migrated route individually.

Common conversion issues

The most common Next.js App Router errors during a Bolt migration are: (1) direct window or localStorage access in a file that doesn't have "use client" at the top (fix: add the directive, or guard the access with a typeof window !== "undefined" check); (2) hooks called in a non-client component (fix: add "use client"); (3) importing react-router-dom that you forgot to remove (fix: uninstall it and fix the imports the compiler flags); (4) missing key props in lists the AI generated carelessly (fix: add them — Next.js is stricter than Vite about this).

4. Update environment variables

Bolt and Vite use the VITE_ prefix for any environment variable that should be exposed to the client bundle. Next.js uses NEXT_PUBLIC_ for the same purpose. This is the single most common cause of a migrated app crashing on first run: the code references import.meta.env.VITE_SUPABASE_URL and it's undefined, because Next.js never reads variables with the VITE_ prefix.

Do a global find-replace across the codebase:

Then go through the .env file and rename every variable to match. A variable like VITE_SUPABASE_ANON_KEY becomes NEXT_PUBLIC_SUPABASE_ANON_KEY. Any VITE_SITE_URL becomes NEXT_PUBLIC_SITE_URL.

Server-only variables

This is where founders leak credentials to the browser. The NEXT_PUBLIC_prefix tells Next.js “this variable will be embedded in the client bundle, it is safe for the browser to see.” For server-only secrets — your database URL, your Stripe secret key, your Supabase service-role key — drop the prefix entirely. A Supabase service-role key named NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY is a security incident waiting to happen. The correct name is SUPABASE_SERVICE_ROLE_KEY, no prefix.

Create two files locally: .env.local for your dev secrets (this is in .gitignore by default) and an entry in your password manager for the production values. Add the production values to the Vercel project settings (covered in section 8). Never commit a .env file with real credentials.

5. Database: Supabase and Neon

The database itself usually doesn't move during migration — it already lives on a provider you can point at from anywhere. What moves is how your code connects to it, and this is where Bolt apps tend to have the most accumulated security debt.

If you're on Supabase

Most Bolt apps with auth and data use Supabase. Find the Supabase client initialisation — usually in src/lib/supabase.ts — and update it:

import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

For any code that runs server-side (API routes, Server Components, Server Actions) and needs elevated permissions, create a separate server-only client using the service-role key:

// src/lib/supabase-admin.ts — SERVER ONLY
import { createClient } from "@supabase/supabase-js";

export const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
);

Now the important part: audit Row Level Security. Open the Supabase dashboard, go to Authentication → Policies, and look at every table. Bolt apps frequently ship with RLS either disabled or with policies so permissive they may as well be disabled. Every table with user data must have RLS enabled and explicit SELECT, INSERT, UPDATE, and DELETE policies that filter on auth.uid(). If you skip this step, your Next.js migration ships the same security holes the Bolt app had. See our Supabase RLS guide for non-technical founders for specifics.

If you're on Neon

Neon apps typically use Drizzle, Prisma, or raw pg. Update the connection string env var name to DATABASE_URL (no NEXT_PUBLIC_ prefix — a database URL exposed in the client bundle is a full credential leak). If the Bolt app was accessing the database directly from React components, that code has to move into API routes or Server Actions during migration — Next.js will not (and should not) let you connect to Postgres from a browser-rendered component.

Run your ORM's migration command against the Neon database from your new Next.js project to confirm the connection works: drizzle-kit push or prisma migrate deploy depending on your setup. If it errors, the issue is almost always either the env var or an IP-allow-list restriction on the Neon branch.

6. Auth migration

Bolt apps ship auth in one of three ways: Supabase Auth, Clerk, or a hand-rolled OAuth flow. Each migrates differently.

Supabase Auth

In Next.js App Router, Supabase Auth requires the SSR package to handle sessions correctly across server components, client components, and middleware. Install @supabase/ssr, set up a middleware.ts at the project root to refresh the session on every request, create helper functions for createServerClient and createBrowserClient, and update every place in the code that referenced the old client. The Supabase docs have a canonical Next.js setup guide that you should follow exactly — do not improvise.

After the code migration, update the OAuth redirect URLs in the Supabase dashboard (Authentication → URL Configuration). The Site URL should be your production domain (e.g. https://yourapp.com) and the redirect allow-list should include https://yourapp.com/auth/callback. If any URL in the allow-list still points at a StackBlitz preview, remove it — an orphaned StackBlitz URL in the OAuth allow-list is a footgun.

Clerk

Replace the Bolt/Vite Clerk package (@clerk/clerk-react) with the Next.js version (@clerk/nextjs). Wrap your root layout in <ClerkProvider>, add the middleware for session handling, and update redirect URLs in the Clerk dashboard. The API shape is similar but not identical — hooks like useUser still work, but the provider setup and the way sessions propagate to server components are different.

Hand-rolled OAuth

If the Bolt app implemented OAuth directly against a provider (Google, GitHub, Microsoft), the callback URL registered with that provider needs to be updated. Go to the provider's console (Google Cloud Console for Google, GitHub Developer Settings for GitHub) and change the authorised redirect URI from the StackBlitz preview domain to your production domain. Add the localhost equivalent too, for local development.

Bolt auth bugs to fix during migration

While you're in the auth code, these are the bugs we find in almost every Bolt app: (1) password reset emails pointing at the StackBlitz preview URL (fix: update the email template in Supabase or Clerk to use a variable for the base URL); (2) no onAuthStateChangelistener, so the UI doesn't update after login/logout without a page refresh; (3) session tokens not refreshing on long-lived sessions, so users get silently logged out after an hour; (4) no handling for the “email not confirmed” state, so users who signed up but didn't click the verification link hit a confusing empty screen.

7. Stripe and payments

Payments are the second-most-common place Bolt migrations break, after auth. The checkout flow itself usually migrates cleanly — it's just a React form that calls stripe.redirectToCheckout() or a Checkout Session API route. The webhook handler and the URLs registered with Stripe are where the work is.

Move the webhook handler into a Next.js API route

Create src/app/api/webhooks/stripe/route.ts. The handler reads the raw request body, verifies the signature with your webhook signing secret, and dispatches on event type:

import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: NextRequest) {
  const sig = req.headers.get("stripe-signature");
  const body = await req.text();

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      sig!,
      process.env.STRIPE_WEBHOOK_SECRET!,
    );
  } catch {
    return NextResponse.json({ error: "invalid signature" }, { status: 400 });
  }

  // Dispatch on event.type — see below
  return NextResponse.json({ received: true });
}

Note that Next.js App Router route handlers receive the raw body via req.text(), not a parsed JSON body — Stripe signature verification requires the raw bytes exactly as sent.

Update the webhook endpoint in the Stripe dashboard

In the Stripe dashboard, go to Developers → Webhooks. Find the endpoint that points at your StackBlitz preview URL. Change it to https://yourapp.com/api/webhooks/stripe. Copy the new signing secret (Stripe rotates it when you update the endpoint) and add it as STRIPE_WEBHOOK_SECRET in your env vars.

Test with the Stripe CLI

Before deploying, test the webhook locally:

stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completed
stripe trigger invoice.payment_failed
stripe trigger customer.subscription.deleted

Watch the logs. Confirm that each event produces the expected database state change. This is usually the step that reveals missing webhook handlers — Bolt apps almost always handle checkout.session.completed but ignore invoice.payment_failed and customer.subscription.deleted, which are the events that matter for keeping your subscription state in sync over time. Our Stripe checklist covers the full event surface.

8. Deploy to Vercel

Vercel is the natural hosting target for Next.js — they build the framework — and the deploy flow is as smooth as anything on the market. The steps:

  1. Go to vercel.com, click “Add New Project,” and connect the GitHub repo you pushed in step 2.
  2. Vercel auto-detects Next.js and fills in the build command (next build) and output directory. Leave the defaults.
  3. Open the Environment Variables section and add every variable from your .env.local. Do this carefully — missing env vars are the number-one cause of first-deploy failures. Scope server-only secrets (database URL, Stripe secret key, Supabase service-role key) to “Production” and “Preview,” never exposed to the client.
  4. Click “Deploy.” The first build takes 2–5 minutes. Watch the build logs for errors.
  5. Once deployed, go to Settings → Domains and add your custom domain. Vercel provisions an SSL certificate automatically.
  6. Update the Site URL in Supabase or Clerk, the OAuth redirect URIs in your identity providers, and the Stripe webhook endpoint to point at the new production domain.

Common first-deploy failures

Once the first deploy is green and the smoke tests pass (sign up, sign in, complete a checkout, trigger a failed payment, reset a password), the migration is technically done. What usually follows is a hardening pass — RLS audit, webhook idempotency, observability setup — that takes the app from “migrated” to “production-ready.” That pass is worth doing before you advertise the new version to customers.

See how we scope Bolt migrations →

FAQ
How long does a Bolt to Next.js migration take?
For a simple app (under 20 components, Supabase, no custom backend): 2–5 days. For a complex app with multiple auth providers, Stripe, custom API routes, and significant business logic: 1–2 weeks. The Vite → Next.js conversion and the auth/payments testing are the time-intensive parts.
Do I need to rewrite all my components?
No — React components are portable. The main work is: updating routing (React Router → Next.js App Router), updating env var prefixes (VITE_ → NEXT_PUBLIC_), adding 'use client' to components with hooks, and updating the Supabase/auth client initialisation.
Can I keep the Bolt.new subscription during the migration?
Yes, and you should. Keep Bolt running as a reference for how the app works and as a fallback if the migration reveals unexpected behaviour. Cancel after the Next.js version is live and validated.
What if my Bolt app uses a backend I don't control?
Some Bolt apps use external APIs or custom backends outside StackBlitz. These usually survive migration without changes — they're just REST or GraphQL endpoints. The env vars that point at them need the prefix update, and if they have OAuth callbacks, those need the production URL update.
My Bolt app is in the fix loop. Should I migrate before fixing it?
Fix first, then migrate. Migrating a broken app produces a broken Next.js app. Get the core features working in Bolt, then migrate to own the infrastructure. If you can't fix it in Bolt, bring a developer in — we do this combination engagement regularly.
Does Afterbuild Labs do Bolt to Next.js migrations?
Yes. We scope them fixed-fee. A typical Bolt migration with hardening (RLS, auth, Stripe webhooks) is $3,999–$7,499 depending on complexity. Free audit first — we tell you exactly what's involved before any money changes hands.
Next step

Migrate your Bolt app to Next.js

We scope it fixed-fee, migrate the code, fix the auth and payments gaps, and hand you a production-ready Next.js app. Free audit in 48 hours.

Book free diagnostic →