Skip to main content

What Breaks

The CLI handles the mechanical transformation, but some Lovable-specific patterns require manual fixes. Here’s what to watch for.

Hardcoded API Calls

Lovable-Managed Endpoints

Lovable apps often have hardcoded references to Lovable’s managed backend:
// ❌ Hardcoded Lovable endpoints (these won't work outside Lovable)
const fetchData = async () => {
  const response = await fetch('/api/projects');
  // This endpoint is Lovable's managed API
};

const uploadImage = async (file) => {
  const response = await fetch('/api/upload', {
    method: 'POST',
    body: file,
  });
  // This goes to Lovable's storage
};
Fix: Update to your own API or Supabase:
// ✅ Your own Supabase instance
const fetchData = async () => {
  const { data, error } = await supabase
    .from('projects')
    .select('*');
  
  if (error) throw error;
  return data;
};

const uploadImage = async (file) => {
  const { data } = await supabase
    .storage
    .from('images')
    .upload(`public/${file.name}`, file);
  
  return data;
};

Supabase Client Configuration

// ❌ Hardcoded Lovable Supabase client
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  'https://lovable-project.supabase.co', // Lovable's instance
  'public-anon-key'
);
Fix: Update to your own Supabase project:
// ✅ Your own Supabase project
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL, // Your project URL
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);

Environment Variables

Lovable-Specific Variables

Lovable injects environment variables at build time that won’t exist in your Next.js app:
# ❌ Won't work outside Lovable
VITE_SUPABASE_URL=https://lovable-project.supabase.co
VITE_SUPABASE_ANON_KEY=lovable-anon-key
Fix: Create your own .env.local:
# ✅ Your own variables (Next.js format)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
Prefix changes:
  • Vite uses VITE_ prefix
  • Next.js uses NEXT_PUBLIC_ prefix for client-side env vars

Missing Environment Variables

After conversion, your app won’t have access to Lovable’s injected variables. You’ll need to:
  1. Create .env.local file
  2. Add your Supabase credentials
  3. Update any hardcoded URLs
  4. Restart dev server

Route Structure Changes

React Router → Next.js App Router

The CLI converts React Router patterns, but you need to understand the new structure:
// ❌ React Router setup (your old code)
const router = createBrowserRouter([
  {
    path: '/',
    element: <Home />,
    children: [
      { path: 'dashboard', element: <Dashboard /> },
      { path: 'profile/:id', element: <Profile /> },
    ],
  },
]);
Becomes: File-based routing in app/ directory
app/
├── page.tsx              # / (home)
├── layout.tsx            # Root layout
├── dashboard/
   └── page.tsx          # /dashboard
└── profile/
    └── [id]/
        └── page.tsx      # /profile/:id

Route Parameters

// ❌ React Router params
import { useParams } from 'react-router-dom';

const Profile = () => {
  const { id } = useParams();
  // id comes from URL
};
// ✅ Next.js App Router params
// File: app/profile/[id]/page.tsx
export default function Profile({ params }: { params: { id: string } }) {
  const { id } = params;
  // id comes from params prop
}

Data Fetching Patterns

Client-Side Fetching in useEffect

// ⚠️ Works but not optimal
'use client';

const Dashboard = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(setData)
      .finally(() => setLoading(false));
  }, []);
  
  if (loading) return <div>Loading...</div>;
  return <div>{data.map(...)}</div>;
};
Optimization: Use Server Components for data fetching:
// ✅ Server Component (no 'use client')
// Direct data fetching on the server
const Dashboard = async () => {
  const data = await fetch('https://api.example.com/data', {
    // This happens server-side, API key stays hidden
    headers: { 'Authorization': `Bearer ${process.env.API_KEY}` }
  });
  
  return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>;
};

Realtime Subscriptions

// ⚠️ Needs client directive for realtime
'use client';

import { useEffect } from 'react';

const RealtimeComponent = () => {
  useEffect(() => {
    const subscription = supabase
      .channel('table-changes')
      .on('postgres_changes', { event: '*', schema: 'public', table: 'items' },
        (payload) => console.log(payload)
      )
      .subscribe();
    
    return () => subscription.unsubscribe();
  }, []);
};

Authentication Patterns

Client-Side Auth State

// ⚠️ Works but flashes loading state
'use client';

const ProtectedPage = () => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    supabase.auth.getUser().then(({ data }) => {
      setUser(data.user);
      setLoading(false);
    });
  }, []);
  
  if (loading) return <div>Loading...</div>;
  if (!user) return <div>Please sign in</div>;
  
  return <div>Protected content</div>;
};
Better: Use middleware for route protection:
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
import { NextResponse } from 'next/server';

export async function middleware(req) {
  const res = NextResponse.next();
  const supabase = createMiddlewareClient({ req, res });
  const { data: { session } } = await supabase.auth.getSession();
  
  // Protect dashboard routes
  if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
  
  return res;
}

export const config = {
  matcher: ['/dashboard/:path*']
};

Database Queries

Hardcoded Table Names

Make sure your table names match your Supabase schema:
// ❌ May not work if table names differ
const { data } = await supabase
  .from('Projects') // Case sensitive!
  .select('*');

// ✅ Use exact table names from your schema
const { data } = await supabase
  .from('projects') // lowercase, matching your migrations
  .select('*');

RLS Policy Dependencies

If you’re still on Lovable’s backend temporarily:
// ⚠️ RLS policies on Lovable may block requests
const { data, error } = await supabase
  .from('private_data')
  .select('*');

// Check if RLS is blocking you
if (error && error.code === 'PGRST301') {
  console.log('RLS policy blocking access');
}
Fix: Either migrate to your own Supabase with proper RLS, or use service role key for admin operations.

Styling Issues

Tailwind Classes

Most Tailwind classes work, but check for:
/* ❌ Custom Lovable classes may not exist */
.bg-lovable-primary { /* Won't work */ }

/* ✅ Replace with standard Tailwind or your config */
.bg-blue-600 { /* Standard Tailwind */ }

Font Imports

/* ❌ Remove Lovable-specific font imports */
@import url('https://lovable-fonts.example.com/custom-font.css');

/* ✅ Use standard font imports */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');

Image Handling

Standard img Tags

// ⚠️ Works but not optimized
<img src="/photo.jpg" alt="Photo" width={500} height={300} />
Better: Use Next.js Image component:
// ✅ Optimized with Next.js Image
import Image from 'next/image';

<Image 
  src="/photo.jpg" 
  alt="Photo" 
  width={500} 
  height={300}
  priority={true} // For above-fold images
/>

External Images

// ❌ External images need domain config
<Image src="https://external-site.com/photo.jpg" ... />

// ✅ Add to next.config.js
// next.config.js
module.exports = {
  images: {
    domains: ['external-site.com'],
  },
};

Build Errors

”window is not defined”

Cause: Browser API used in Server Component
// ❌ This fails in Server Component
const width = window.innerWidth;
Fix: Add ‘use client’ or use dynamic import:
// ✅ Option 1: Make it a Client Component
'use client';

const MyComponent = () => {
  const width = window.innerWidth;
  // ...
};
// ✅ Option 2: Dynamic import with ssr: false
import dynamic from 'next/dynamic';

const ClientComponent = dynamic(() => import('./ClientComponent'), {
  ssr: false,
});

“document is not defined”

Same fix as above - either add ‘use client’ or ensure code runs only in browser.

”Cannot find module”

Cause: Old import paths from Vite structure
// ❌ Old Vite paths may not resolve
import { Button } from '@/components/ui/button';
// If @ alias isn't configured in Next.js
Fix: Update tsconfig.json or next.config.js:
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  }
}

Post-Conversion Checklist

1

Update environment variables

Create .env.local with your Supabase credentials
2

Update Supabase client config

Change from Lovable’s Supabase URL to your own
3

Update API endpoints

Change any hardcoded /api/ calls to your own backend
4

Review data fetching

Consider moving fetch calls to Server Components
5

Add middleware for auth

Protect routes at the edge instead of client-side
6

Test auth flow

Verify login/logout works with your Supabase
7

Update image components

Replace &lt;img&gt; with Next.js &lt;Image&gt;
8

Run the build

npm run build to catch any remaining issues
9

Test in production mode

npm start after building to test the real thing

Common Error Messages

”Failed to fetch”

Cause: API endpoint doesn’t exist or CORS error Fix: Check that your Supabase URL is correct and accessible

”Auth session missing”

Cause: Auth state not persisting or wrong Supabase project Fix: Verify you’re connecting to the right Supabase instance

”Row level security violation”

Cause: RLS policies blocking your query Fix: Check RLS policies in your Supabase dashboard, or use service role for admin ops

”relation does not exist”

Cause: Table name typo or schema mismatch Fix: Check exact table names in Supabase, case-sensitive!
Still stuck? Check the Troubleshooting guide or use our Cloud Migration Extension to move your backend too.