Skip to content
back to journal

general

From Lovable to Next.js: A Senior Engineer's Migration Playbook

Honest, phase-by-phase guide to migrating a Lovable SPA to Next.js — covering SSR, Supabase SSR auth, SEO, and real time estimates by app size.

Ralph DuinApril 24, 202618 min read
<p>Migrating a Lovable app to Next.js is a six-phase engineering project, not a button click. If you are a founder or PM who has hit Lovable's ceiling and is wondering whether the effort is worth it, this post gives you an honest account of what changes, what it costs, and where things go wrong. The short answer: it is almost always doable, it is never trivial, and for most apps it takes one to three months of real engineering time.</p> <p>Lovable generates React single-page applications built on Vite. Next.js is a full-stack meta-framework with server-side rendering, file-system routing, React Server Components, and a built-in API layer. The surface area difference is real. The good news is that Lovable's output is standard React and TypeScript, so most of your components copy across with minimal changes. The bad news is that routing, data fetching, authentication patterns, and environment variable conventions all need to change. This guide walks you through each phase in the order that minimises pain and avoids regression.</p> <h2>When staying on Lovable is the right call</h2> <p>Before you plan a migration, be honest about whether you actually need one. There are at least three situations where staying on Lovable is the correct engineering decision.</p> <h3>You are still iterating on the product</h3> <p>If your product direction is still changing week to week, migrating to Next.js will slow you down significantly. Lovable lets a non-engineer make product changes without engineering involvement. That speed is worth real money in the early stage. Wait until the core flows are stable before spending engineering capital on infrastructure.</p> <h3>SEO is not a priority channel for your business</h3> <p>The most common reason people migrate is SEO: Lovable outputs a client-rendered SPA, which means Googlebot gets an empty HTML shell and has to execute JavaScript to see your content. For marketing pages this matters. For SaaS products where most content lives behind a login it matters very little. If your growth is word-of-mouth, paid, or outbound, you can stay on Lovable without any SEO penalty worth worrying about.</p> <h3>Your team has no Next.js experience</h3> <p>A Next.js codebase is meaningfully more complex to operate than a Vite SPA. If the engineer doing the migration has not shipped a production Next.js app before, budget two to three times the hours you think it will take. If nobody on the team has that experience, consider a fractional hire to at least anchor the architecture before handing it off. A bad Next.js migration produces a codebase that is harder to maintain than the Lovable app it replaced.</p> <h2>Signals it is time to migrate to Next.js</h2> <p>With that caveat in place, here are the five concrete signals that a migration is genuinely warranted.</p> <h3>SEO is a priority and pages are not indexing</h3> <p>If you are running a content strategy, a blog, a landing page for every city or use case, or anything where organic search matters, a client-rendered SPA is a structural problem. Google's crawler can execute JavaScript, but it does so inconsistently and with a delay. Server-side rendering or static generation in Next.js guarantees that the crawler sees the same HTML a user does, with no JavaScript required. If you are checking Google Search Console and seeing crawl errors, low coverage, or pages stuck in "Discovered - currently not indexed", the SPA architecture is likely a contributor.</p> <h3>Core Web Vitals and Largest Contentful Paint are failing</h3> <p>Lovable apps bundle everything for the browser. As your app grows, the JavaScript bundle grows with it, and LCP on a cold load degrades. Next.js lets you split routes, stream server components, and defer client-side JavaScript, which gives you structural tools to fix LCP without heroic optimisation effort. If your PageSpeed Insights score is in the red and the waterfall shows a large JS parse on first load, a framework change is a more durable fix than chasing bundle size.</p> <h3>You need custom backend logic that Supabase Edge Functions cannot cleanly handle</h3> <p>Supabase Edge Functions are useful but they are not a full backend. If you need webhooks with complex routing, background jobs, PDF generation, custom auth middleware, or third-party API orchestration, Next.js API routes (or Route Handlers in the App Router) give you a proper server context that co-locates with your frontend code. The deployment is simpler and the debugging experience is significantly better than trying to wire everything through Edge Functions.</p> <h3>You are hiring engineers who push back on the codebase</h3> <p>This is a soft signal but a real one. Senior engineers who interview for a role and see a Lovable-generated Vite SPA with no CI, no type-safe data layer, and Supabase access directly in React components will have concerns. That does not mean the Lovable app is bad - it means a real engineering team expects a real engineering codebase. If hiring is blocked or retention is at risk because of the stack, that is a legitimate business reason to migrate.</p> <h3>Vendor lock-in is becoming a board-level concern</h3> <p>Lovable is a small company. Its product direction, pricing, and continued operation are outside your control. Most of its output is standard code you can export, but your development workflow, AI-assisted iteration, and deployment pipeline are all tied to the platform. For early-stage companies this tradeoff is fine. For companies with investors, revenue, and a long-term roadmap, owning a standard codebase on a commodity hosting platform is a lower-risk position.</p> <h2>What Lovable gives you (and what it does not)</h2> <p>Before planning the migration, do an honest inventory of what you are keeping, what you are replacing, and what Lovable never gave you in the first place. The table below covers the main areas.</p> <table> <thead> <tr> <th>Area</th> <th>Lovable SPA</th> <th>Next.js (App Router)</th> <th>Migration effort</th> </tr> </thead> <tbody> <tr> <td>UI components</td> <td>React + shadcn/ui + Tailwind</td> <td>Same - copy across</td> <td>Low</td> </tr> <tr> <td>Client-side routing</td> <td>react-router-dom</td> <td>File-system (App Router)</td> <td>Medium - manual port</td> </tr> <tr> <td>Data fetching</td> <td>useEffect + supabase-js</td> <td>Server components + service-role client</td> <td>Medium to high</td> </tr> <tr> <td>Authentication</td> <td>Supabase Auth (client-side)</td> <td>Supabase SSR (cookie-based sessions)</td> <td>Medium</td> </tr> <tr> <td>Database</td> <td>Supabase (Postgres)</td> <td>Same Supabase project</td> <td>None - you keep it</td> </tr> <tr> <td>SEO / metadata</td> <td>Limited - SPA only</td> <td>Full - Metadata API, sitemap.ts, structured data</td> <td>Low once on Next.js</td> </tr> <tr> <td>Image optimisation</td> <td>None built in</td> <td>next/image with automatic WebP/AVIF</td> <td>Low - swap tags</td> </tr> <tr> <td>API routes / backend</td> <td>Supabase Edge Functions</td> <td>Route Handlers co-located in /app</td> <td>Low to medium</td> </tr> <tr> <td>Payments</td> <td>Stripe (if added)</td> <td>Same Stripe integration, webhook handler moves to Route Handler</td> <td>Low</td> </tr> <tr> <td>CI/CD</td> <td>Lovable deploy pipeline</td> <td>Vercel, Fly.io, or custom GitHub Actions</td> <td>Medium - new pipeline</td> </tr> <tr> <td>Team collaboration</td> <td>Lovable AI editor</td> <td>Standard git workflow</td> <td>Process change, not technical</td> </tr> <tr> <td>Caching / ISR</td> <td>None</td> <td>Full - fetch cache, revalidatePath, ISR</td> <td>Low - additive only</td> </tr> </tbody> </table> <p>The honest summary: the UI layer migrates cleanly because Lovable uses standard tooling. The data layer and auth patterns take the most time because they require understanding React Server Components and Supabase's SSR client, which behave differently from the client-side patterns Lovable generates. See our <a href="/blog/rescue-your-lovable-app">guide to rescuing a Lovable app</a> for a deeper look at the specific issues that show up in Lovable production apps before you commit to a full rewrite.</p> <h2>The migration plan - phase by phase</h2> <p>This is the sequence we use. Do not skip phases or run them in parallel. Each phase de-risks the next.</p> <h3>Phase 0: Inventory</h3> <p>Before writing a single line of Next.js code, document what you are migrating. This sounds obvious but most failed migrations skip it.</p> <ul> <li><strong>Routes:</strong> List every page in your react-router-dom config. Note which are public, which require auth, which are dynamic (e.g. <code>/project/:id</code>).</li> <li><strong>API calls:</strong> Search your codebase for every <code>supabase.from(</code> and <code>supabase.functions.invoke(</code> call. Note the table, the operation (select/insert/update/delete), and whether it runs inside a component or in an event handler.</li> <li><strong>Environment variables:</strong> List every <code>VITE_</code> prefixed variable. These become <code>NEXT_PUBLIC_</code> in Next.js. Server-only values lose the prefix entirely.</li> <li><strong>Supabase schema:</strong> Export your schema DDL from Supabase's SQL editor. You will not migrate the database, but you need to know which tables have RLS enabled and what the policies are, because server components bypass RLS if you use the service-role key carelessly.</li> <li><strong>Third-party integrations:</strong> Stripe webhooks, email providers, OAuth redirect URIs. Each has a config change when your domain structure changes.</li> </ul> <p>The output of Phase 0 is a spreadsheet or doc you will check off as you work through Phases 1-5. Without it you will miss routes and ship a broken redirect on launch day.</p> <h3>Phase 1: Scaffold Next.js 15 with App Router and Supabase SSR</h3> <p>Create the Next.js project alongside your existing repo (not replacing it). Keep the Lovable app running in production throughout the migration.</p> <pre><code>npx create-next-app@latest my-app --typescript --tailwind --app cd my-app npm install @supabase/supabase-js @supabase/ssr</code></pre> <p>The critical setup is the Supabase SSR client, which uses cookies instead of localStorage for session management. Create two clients - one for server contexts (Server Components, Route Handlers, Server Actions) and one for the browser.</p> <pre><code>// lib/supabase/server.ts import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' export async function createClient() { const cookieStore = await cookies() return createServerClient( process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll() }, setAll(cookiesToSet) { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options)) }, }, } ) }</code></pre> <p>Install shadcn/ui into the new project using its CLI. Your existing component code will be compatible, but shadcn components are not copy-paste portable as files - reinstall them fresh so the config and dependencies are correct for your Next.js version.</p> <h3>Phase 2: Port the UI route by route</h3> <p>Work through your Phase 0 route inventory one route at a time. Create the corresponding <code>app/</code> directory structure. For a Lovable route like <code>/dashboard</code> you create <code>app/dashboard/page.tsx</code>. For <code>/project/:id</code> you create <code>app/project/[id]/page.tsx</code>.</p> <p>Copy the JSX from your Lovable components into the new files. At this stage, keep components as client components (<code>'use client'</code>) so you do not have to change data fetching patterns yet. The goal of this phase is visual parity, not architectural improvement. You can refactor to server components in Phase 3.</p> <p>Replace all <code>react-router-dom</code> imports: <code>Link</code> comes from <code>next/link</code>, <code>useNavigate</code> becomes <code>useRouter</code> from <code>next/navigation</code>, and <code>useParams</code> stays <code>useParams</code> but imports from <code>next/navigation</code> instead. The <code>useSearchParams</code> hook requires a Suspense boundary in App Router - wrap any component that uses it.</p> <h3>Phase 3: Convert data fetching to server components</h3> <p>This is the most time-consuming phase and the one most likely to introduce bugs. Take it slowly.</p> <p>The pattern shift: in Lovable, data fetching looks like a <code>useEffect</code> that calls <code>supabase.from('projects').select()</code> inside a client component. In Next.js, the same data can be fetched in a Server Component that runs on the server, with no <code>useEffect</code>, no loading state, and no client JavaScript.</p> <pre><code>// app/dashboard/page.tsx (Server Component - no 'use client') import { createClient } from '@/lib/supabase/server' export default async function DashboardPage() { const supabase = await createClient() const { data: projects } = await supabase .from('projects') .select('id, name, created_at') .order('created_at', { ascending: false }) return ( &lt;ul&gt; {projects?.map(p =&gt; ( &lt;li key={p.id}&gt;{p.name}&lt;/li&gt; ))} &lt;/ul&gt; ) }</code></pre> <p>Not everything belongs in a server component. Any component that uses browser APIs, React hooks, event handlers, or state must remain a client component. The practical split: fetch data in a server component, pass it as props to client components that handle interactivity.</p> <p>Move Supabase Edge Functions to Route Handlers where it makes sense. A webhook handler at <code>app/api/stripe/webhook/route.ts</code> replaces an Edge Function and is easier to test locally and deploy.</p> <h3>Phase 4: SEO layer</h3> <p>This is the payoff for the migration effort. Next.js gives you the Metadata API, <code>sitemap.ts</code>, <code>robots.ts</code>, and structured data - all rendered server-side and available to crawlers immediately.</p> <p>Add a <code>generateMetadata</code> export to every page that needs unique title and description tags. For dynamic pages, fetch the data inside <code>generateMetadata</code> - Next.js deduplicates the fetch so you are not hitting the database twice.</p> <p>Add <code>app/sitemap.ts</code> to generate a dynamic XML sitemap from your database. Add <code>app/robots.ts</code> to set crawl rules. If your content warrants structured data (articles, products, FAQs), add JSON-LD as a server-rendered <code>script</code> tag in each page's layout.</p> <p>For a deeper look at SEO patterns specific to apps that started on Lovable, see our post on <a href="/blog/seo-for-lovable-apps">SEO for Lovable apps</a>.</p> <h3>Phase 5: Deploy and DNS cutover</h3> <p>Deploy the Next.js app before you cut DNS. Run both apps in parallel against the same Supabase project for at least a week of testing.</p> <p>Vercel is the path of least resistance for Next.js deployment - zero config, automatic preview deployments, built-in image optimisation, and Edge Network for static assets. If you need more control or are self-hosting, we use <a href="https://fly.io">Fly.io</a> for containerised Next.js deployments and Cloudflare Workers with Next-on-Pages for edge-rendered apps. Each has tradeoffs: Vercel is fast to ship, Fly.io is more flexible for background jobs and WebSockets, Cloudflare Workers require careful attention to the Node.js API subset available at the edge.</p> <p>Update OAuth redirect URIs in Supabase (and any third-party providers like Google or GitHub) before switching DNS. Update Stripe webhook endpoints. Run your full test suite against the new domain on a staging environment before the cutover. Set up 301 redirects for any URLs that change structure.</p> <h2>Gotchas the tutorials do not mention</h2> <p>These are the issues that show up in real migrations that are not covered in the official Next.js or Supabase docs.</p> <h3>Supabase RLS behaves differently under SSR</h3> <p>In your Lovable app, every Supabase call runs in the browser with the user's session attached automatically via localStorage. In a server component, the session comes from the cookie store. If the cookie is not set or is expired, your server component runs as an unauthenticated request and RLS policies that depend on <code>auth.uid()</code> return no rows silently rather than throwing an error. This means a bug in your auth middleware does not crash the app - it just shows empty content. Always test authenticated server component pages with an expired or missing session cookie before going to production.</p> <p>If you use the service-role key (for admin operations), RLS is bypassed entirely. This is intentional and sometimes necessary, but if you accidentally use the service-role key in a route that serves user-facing data, you expose data across users. Keep the service-role key strictly server-side and only in contexts where you have validated the user's identity via a separate check.</p> <h3>Hooks that break in server components</h3> <p>React hooks (<code>useState</code>, <code>useEffect</code>, <code>useContext</code>, <code>useRef</code>) are not available in server components. The compiler error is clear, but the fix is not always obvious. Components that use hooks must be marked <code>'use client'</code>. If a third-party library you are using internally calls hooks without exporting client components, you need to wrap it in a client boundary. Check every component you copy from Lovable for hook usage before assuming it can become a server component.</p> <h3>Environment variable prefix changes</h3> <p>Lovable uses Vite, which exposes browser-safe variables with the <code>VITE_</code> prefix. Next.js uses <code>NEXT_PUBLIC_</code>. Variables without either prefix are server-only and cannot be accessed in client components. The migration is straightforward but easy to miss: do a global search for <code>import.meta.env.VITE_</code> and replace each one. Variables that should be server-only (Supabase service-role key, Stripe secret key, any API secret) should lose the prefix entirely - if they currently have <code>VITE_</code>, that means they were shipping to the browser in your Lovable app, which is a security issue worth fixing in the migration.</p> <h3>Image loader configuration</h3> <p>If you reference images from external domains (Supabase Storage, Cloudinary, a CDN), you must whitelist those domains in <code>next.config.ts</code> under <code>images.remotePatterns</code>. Next.js will refuse to optimise images from unlisted domains and will throw a build error. Pull your image domains from your Phase 0 inventory and add them before you start swapping <code>img</code> tags for <code>next/image</code>.</p> <h3>shadcn/ui version drift</h3> <p>If your Lovable app is six months old, the shadcn/ui components inside it may be multiple versions behind the current CLI output. When you reinstall shadcn components fresh into your Next.js project, the generated code may differ from what Lovable generated. The differences are usually minor (import paths, variant names) but if you have customised the component internals in Lovable, those changes will not survive the reinstall. Take a diff of each component before copying any customisations across.</p> <h2>How long does it really take?</h2> <p>These are real ranges based on production migrations, not estimates from people who have not done it. They assume a competent Next.js engineer working full-time on the migration.</p> <ul> <li><strong>Small app (under 10 routes, minimal custom logic, Supabase auth only):</strong> 1-2 weeks solo. This is a straightforward port. The routing and data fetching changes are mechanical once you understand the patterns.</li> <li><strong>Medium app (10-30 routes, some Edge Functions, Stripe, one or two complex features):</strong> 3-6 weeks for one engineer. The complexity is in the data fetching conversion and making sure the auth middleware handles all the edge cases (expired tokens, concurrent requests, redirect loops).</li> <li><strong>Complex app (30+ routes, custom backend logic, multiple integrations, background jobs, multi-tenancy):</strong> 2-3 months for a small team (2 engineers). The migration itself is manageable but testing, QA, and running both apps in parallel for a safe cutover takes time you cannot compress.</li> </ul> <p>Add 20-30% to any estimate if you are also fixing pre-existing issues (RLS gaps, missing error handling, no CI pipeline) that you discover during the Phase 0 inventory. Those are worth fixing in the migration rather than carrying forward, but they have real time costs.</p> <h2>Cost of not migrating</h2> <p>If the signals are present and you choose to stay on Lovable, the costs are concrete, not hypothetical.</p> <p><strong>SEO loss:</strong> A client-rendered SPA competes at a structural disadvantage for informational keywords. Google can render JavaScript, but its coverage and speed are inconsistent, and it will not prioritise your SPA over a server-rendered competitor page with the same content. If SEO is a channel you are trying to build, every month on the SPA architecture is a month of ranking potential you will not recover when you eventually migrate. The compounding nature of SEO means delayed migration has disproportionate long-term cost.</p> <p><strong>Scaling ceiling:</strong> Lovable's deployment is a static host serving a JavaScript bundle. That scales fine for traffic, but it does not scale for complexity. As you add features, your bundle grows, your build process gets slower, and the gap between what your product needs and what the architecture supports widens. The longer you wait, the larger the migration scope becomes.</p> <p><strong>Hiring drag:</strong> Engineers who evaluate your codebase during interviews or onboarding will form views about the engineering culture based on what they see. A Lovable-generated codebase without tests, without CI, and with direct Supabase calls in components signals that engineering quality is not a priority. That is a perception issue that affects who accepts offers and who stays. It is correctable, but it takes time to rebuild.</p> <p>If you are evaluating whether migration is worth it for your specific app, the <a href="/fractional-cto">fractional CTO engagement</a> we offer includes a technical audit that gives you a concrete scope and time estimate before you commit engineering budget.</p> <h2>FAQs</h2> <h3>Can I keep Supabase after migrating to Next.js?</h3> <p>Yes, and you should. Supabase is not a Lovable dependency - it is a standalone product. Your database, auth configuration, storage buckets, and Edge Functions all stay exactly where they are. The only thing that changes is how your application talks to Supabase: you add the <code>@supabase/ssr</code> package and switch from the client-only session management to cookie-based sessions. Your data does not move. Your schema does not change. Your users do not notice.</p> <h3>Does Lovable export clean code?</h3> <p>Mostly, yes. Lovable generates readable TypeScript React code that follows recognisable conventions. It is not obfuscated or proprietary. The issues are not cleanliness - they are architecture: patterns that are sensible for a client-only SPA (useEffect for data fetching, client-side auth checks, direct Supabase calls in components) that need to change when you move to Next.js. Think of it as code that was correct for one context being moved to a different context. You are not cleaning up mess; you are adapting patterns.</p> <h3>Do I lose anything visually in the migration?</h3> <p>No. Tailwind CSS and shadcn/ui work identically in Next.js. Your design system, colours, typography, spacing, and component variants all transfer without changes. The only visual risk is if you have customised component internals in Lovable and those customisations conflict with a reinstalled shadcn component (see the gotchas section above). That is a specific, findable issue, not a general risk.</p> <h3>Can I migrate piece by piece rather than all at once?</h3> <p>In theory, yes. In practice, it is harder than it sounds. The main blocker is authentication: your session is managed either by Lovable's SPA auth pattern or by Next.js's SSR auth pattern, not both simultaneously. Running a hybrid where some routes are on the old app and some are on the new one requires a shared session mechanism, which usually means setting up a subdomain structure (<code>app.yourdomain.com</code> for the old app, routing specific paths to Next.js) and sharing cookies across subdomains. It is doable but it adds complexity and the payoff is marginal. Most teams find it cleaner to do the full migration in a staging environment and cut over in one go.</p> <h3>What happens to my custom domains?</h3> <p>Your domain is yours. It is not managed by Lovable. You point DNS to Lovable today; after the migration you point it to Vercel, Fly.io, Cloudflare, or wherever you deploy the Next.js app. The cutover is a DNS change - typically a few minutes of propagation if you have set a short TTL. Keep the Lovable deployment running and accessible until DNS has fully propagated globally, which can take up to 48 hours in edge cases even with a short TTL set in advance.</p> <p>Update your Supabase auth settings to add the new domain to the allowed redirect URLs before you cut DNS. If you are using Google OAuth or any other third-party provider, update the authorised redirect URIs in their developer consoles as well. Missing this step means your login flow will break immediately after the DNS switch.</p> <h2>When to bring in help</h2> <p>If your Phase 0 inventory turns up more than 20 routes, significant custom auth logic, background job requirements, or an app generating meaningful revenue that cannot tolerate extended downtime, the migration is large enough to warrant outside help.</p> <p>A <a href="/hire-ai-developer">hire-a-developer engagement</a> is the right fit if you need someone to own the migration end to end and hand back a tested, deployed Next.js codebase. A <a href="/fractional-cto">fractional CTO engagement</a> is the right fit if you have an internal engineer who can do the work but needs architecture guidance and someone to de-risk the technical decisions. Both are fixed-scope, not open-ended retainers.</p> <p>If you are not sure whether your app needs a full migration or whether the issues you are hitting can be fixed on Lovable without migrating, read our post on <a href="/blog/rescue-your-lovable-app">rescuing your Lovable app</a> first. Many of the SEO, performance, and security issues that prompt migration consideration can be addressed without changing the framework at all. And if you are evaluating whether to hire an agency or a freelancer for this work, our breakdown of the <a href="/blog/lovable-agency-vs-freelance-lovable-expert">agency vs freelance Lovable expert</a> question covers the tradeoffs in detail.</p> <p>The migration is worth doing when the signals are clear. It is not worth rushing. Take the time to do the Phase 0 inventory properly, and the rest of the project becomes a series of well-scoped engineering tasks rather than a high-risk big-bang rewrite.</p>