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:
- Build system: Vite becomes Next.js. This is the largest single step and affects routing, environment variables, server-side rendering behaviour, and static-asset handling.
- Routing: if the app uses React Router (most Bolt apps do), the routes move to Next.js App Router conventions (file-based routing under
src/app/). - Hosting: StackBlitz preview URL becomes a Vercel deployment on a real domain with CI/CD.
- Environment variables:
VITE_*prefixes becomeNEXT_PUBLIC_*for client-exposed vars. Server-only vars drop the prefix entirely. - Third-party callbacks: any OAuth redirect, Stripe webhook, or email-verification link pointed at the StackBlitz preview domain needs to be repointed at your production domain.
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 devIf 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-appThat 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:
- Dependencies. Open the Bolt
package.jsonand install everything the Bolt app depends on (excludingvite,@vitejs/plugin-react, andreact-router-dom, which Next.js replaces). - Shared utilities and types. Copy
src/lib/,src/utils/, andsrc/types/from Bolt into the new project. These are pure TypeScript and should move without modification. - Components. Copy
src/components/. Most components will work. Any component that usesuseState,useEffect,useRouter,window, orlocalStorageneeds"use client"at the top of the file — these are client-only components in the App Router model. - Routes.For each route in the Bolt app's React Router config, create a matching folder under
src/app/. A route like/dashboard/settingsbecomessrc/app/dashboard/settings/page.tsx. The component that React Router rendered becomes the default export ofpage.tsx. - Layouts. If the Bolt app has a shared layout (nav + footer wrapped around all pages), that becomes
src/app/layout.tsxin Next.js. Nested layouts becomelayout.tsxfiles in subdirectories. - Styling. If the Bolt app uses Tailwind (most do), copy the
tailwind.config.jscontent (colors, fonts, extensions) into the Next.js Tailwind config. Copy any global CSS intosrc/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:
import.meta.env.VITE_→process.env.NEXT_PUBLIC_process.env.VITE_→process.env.NEXT_PUBLIC_
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.deletedWatch 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:
- Go to
vercel.com, click “Add New Project,” and connect the GitHub repo you pushed in step 2. - Vercel auto-detects Next.js and fills in the build command (
next build) and output directory. Leave the defaults. - 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. - Click “Deploy.” The first build takes 2–5 minutes. Watch the build logs for errors.
- Once deployed, go to Settings → Domains and add your custom domain. Vercel provisions an SSL certificate automatically.
- 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
- “Cannot read properties of undefined” during build. A server-rendered page references an env var that isn't set in Vercel. Fix: add the missing var to the Vercel env var settings.
- TypeScript errors that worked in Bolt. StackBlitz was more permissive than a real
tscbuild. Fix the types rather than disabling the check — the errors are real. - Tailwind classes not applying. The
contentglob intailwind.config.jsstill references Vite paths (./src/**/*.tsxis usually fine;./index.htmlis Vite-specific and can be removed). - “use client” missing on a hook-using component. The build produces a clear error pointing at the file; add the directive at the top and redeploy.
- Webhook 403s in production. The Stripe webhook signing secret in Vercel is still the local value from
stripe listen. Fix: copy the production signing secret from the Stripe dashboard and update the env var.
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.