| name | delovable |
| description | Migrate Lovable, Bolt.new or any Vite+React+Supabase project to Next.js 15 App Router + Clerk + Biome + Bun. Use when the user says "migrate from lovable", "convert lovable to nextjs", "delovable", "graduate from lovable", "move off lovable", "lovable to production", or wants to convert a Vite SPA to Next.js. Also trigger when the user has a project with lovable-tagger, vite_react_shadcn_ts, or .bolt/ directory and wants to make it production-ready.
|
| allowed-tools | WebFetch |
delovable
Migrate a vibecoded project (Lovable, Bolt.new, generic Vite+React) to a production-ready Next.js 15 stack.
This is not just a file converter. Each step explains WHY the change matters, connecting to real engineering concepts. The user should understand their code better after the migration, not just have different files.
Target Stack
| From | To |
|---|
| Vite | Next.js 15 (App Router) |
| React Router | File-based routing |
| Supabase Auth | Clerk |
| ESLint/Prettier | Biome |
| npm | Bun |
| SPA (client-only) | Server Components + client where needed |
VITE_* env vars | NEXT_PUBLIC_* + server-only |
Supabase Postgres stays as the database. Data migration is out of scope.
Detection
Before starting, confirm the project is a vibecoded SPA. Check for >= 2 of these:
| Signal | Confidence |
|---|
package.json name is "vite_react_shadcn_ts" | Lovable (certain) |
lovable-tagger in devDependencies | Lovable (certain) |
.lovable/ directory exists | Lovable (certain) |
.bolt/ directory exists | Bolt.new (certain) |
vite + react-router-dom + @supabase/supabase-js in deps | Likely vibecoded |
src/integrations/supabase/client.ts exists | Lovable pattern |
src/pages/Index.tsx (capital I) | Lovable convention |
If none match, ask the user to confirm before proceeding.
Migration Flow
The migration runs in 5 phases. Phases 1 and 5 are automatic. Phases 2, 3, 4 are guided (explain what's happening and why).
Phase 1: Scaffold (automatic)
Create the Next.js project alongside the existing code:
- Run
bunx create-next-app@latest . --ts --tailwind --app --src-dir --import-alias "@/*" --no-eslint --turbopack in a new directory (or transform in-place if the user prefers)
- Install deps:
bun add @clerk/nextjs sonner @tanstack/react-query
- Install Biome:
bun add -d @biomejs/biome
- Copy
src/components/ as-is (shadcn/ui is compatible, components.json just needs "rsc": true)
- Copy
src/lib/utils.ts as-is (the cn() function is universal)
- Copy
src/hooks/ (add 'use client' to each file)
- Copy
src/index.css (Tailwind directives and CSS variables work in Next.js)
- Update
components.json to set "rsc": true
Why scaffold first: The project structure changes fundamentally. Vite uses index.html + main.tsx as entry points. Next.js uses app/layout.tsx + app/page.tsx. Building the new structure first, then migrating content into it, is less error-prone than trying to transform in-place.
Phase 2: Routes (guided)
Read references/route-mapping.md for the full pattern.
The core idea: Lovable puts all pages in src/pages/ and wires them together in App.tsx with React Router. Next.js uses the filesystem itself as the router, each folder in app/ becomes a URL path.
- Parse
App.tsx for <Route path="..." element={<Component />} /> entries
- For each route, create the corresponding
app/{path}/page.tsx
- Move the component content into the page file
- Wrap interactive components with
'use client' where needed (forms, useState, onClick)
- Move provider wrapping (QueryClientProvider, TooltipProvider, Toaster, Sonner) into
app/layout.tsx
Explain to the user:
"Lovable uses React Router, which means all routing logic lives in one App.tsx file. Next.js uses file-based routing, where the folder structure IS the routes. This means adding a new page is just creating a new folder, no router config needed. It also enables each page to be a Server Component by default, which means faster initial loads and better SEO."
Phase 3: Auth (guided)
Read references/auth-migration.md for the full Supabase-to-Clerk conversion.
This is the most impactful change. Lovable uses Supabase Auth with client-side session checks (anon keys in the browser). Clerk uses middleware-based auth (server-side verification before the page even loads).
- Detect auth usage: grep for
supabase.auth, AuthContext, useSession, ProtectedRoute
- Create
middleware.ts with Clerk's clerkMiddleware()
- Add
ClerkProvider to app/layout.tsx
- Replace
supabase.auth.getSession() with Clerk's useUser() / useAuth()
- Replace
<ProtectedRoute> wrappers with Clerk's auth() in server components or <SignedIn> / <SignedOut> components
- Remove
src/contexts/AuthContext.tsx and src/integrations/supabase/client.ts auth references
- Add
.env.local with NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY
Explain to the user:
"Supabase Auth sends an anon key to the browser and checks sessions client-side. This means a user can see your page HTML before auth is verified, and the session token can be tampered with. Clerk's middleware runs on the server before the page loads, so unauthenticated users never see protected content. It's also simpler: no AuthContext, no useEffect for session listening, no manual redirect logic."
Phase 4: Data Layer (guided)
- Find all
supabase.from('table').select() calls
- For calls in components that can be server components: convert to server-side data fetching using
createClient from @supabase/supabase-js with server-only env vars
- For mutations: convert to Server Actions with
'use server'
- Keep
@tanstack/react-query for client-side cache invalidation where needed
Explain to the user:
"In Lovable, every database query runs in the browser, which means the Supabase URL and anon key are exposed in the client bundle. In Next.js, Server Components can fetch data on the server and send only the HTML to the browser. The database credentials never leave the server. This is both more secure and faster, because the server is closer to the database than the user's browser."
Phase 5: Cleanup (automatic)
- Remove Vite files:
vite.config.ts, index.html, src/main.tsx, src/App.tsx, src/App.css
- Remove Vite deps from package.json:
vite, @vitejs/plugin-react-swc, lovable-tagger
- Remove
.lovable/ or .bolt/ directories
- Convert remaining
import.meta.env.VITE_* to process.env.NEXT_PUBLIC_*
- Create
biome.json with standard config
- Run
bunx biome check --write .
- Run
bun dev to verify the app starts
- If build succeeds, suggest:
npx @railly/last30 audit . to check the new score
Post-Migration Checklist
After the migration completes, print this checklist:
Migration complete. Post-migration checklist:
[ ] Set NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in .env.local
[ ] Set CLERK_SECRET_KEY in .env.local
[ ] Set NEXT_PUBLIC_SUPABASE_URL in .env.local (moved from hardcoded)
[ ] Set SUPABASE_SERVICE_ROLE_KEY in .env.local (server-only, was anon key)
[ ] Run `bun dev` and verify all pages load
[ ] Run `npx @railly/last30 audit .` to check your new score
[ ] Deploy to Vercel: `vercel`
Learn more: https://thelast30.dev
What This Skill Does NOT Do
- Database migration (data stays in Supabase)
- Supabase Edge Functions conversion (document as TODO for the user)
- Custom domain setup
- Payment integration
- Testing setup (recommend as next step)
These are covered by The Last 30% course at thelast30.dev.
References
| File | When to read |
|---|
references/route-mapping.md | Phase 2: detailed React Router to App Router patterns |
references/auth-migration.md | Phase 3: Supabase Auth to Clerk step-by-step |