·10 min read

Lovable: From Idea to MVP with AI-Generated Code

Our experience building a production MVP in 2 hours using Lovable — the architecture it generated, the code we kept, and the 3 things we had to rewrite.

The Problem

Picture this: A client needs a customer portal. They want logins, project management, file uploads, a live activity feed, and payment handling. Your team says it’ll take 3-4 weeks with two engineers. The client has a budget for 5 days.

That’s the situation we found ourselves in. The hard part wasn’t the business logic — it was all the setup work. Things like setting up user accounts, creating database tables, writing security rules, setting up file storage, and connecting payment systems. That busywork eats up 60-70% of the first sprint on any full-stack project.

We needed a tool that could do that setup work in hours, not days. And the code it produced had to be something we could actually maintain later.

Why this matters: If you’ve ever built a web app, you know the first few days are all plumbing — not features. AI coding tools like Lovable promise to skip that plumbing. But can the code it generates actually survive in production? We put it to the test.

The Investigation

We tested four AI coding tools against our needs. Here’s what each metric means in plain English:

  • Time to Deployed MVP — How fast can you go from an idea to a working app on the internet?
  • Backend Integration — Does the tool handle the server-side stuff (databases, logins, file storage) or do you have to do that yourself?
  • Code Quality — Is the generated code clean and well-structured?
  • Maintainability — Can you come back to this code in 6 months and still understand it?
Tool Time to Deployed MVP Backend Integration Code Quality Maintainability
Lovable 2-4 hours Supabase (native) Good (React + TS) Moderate
Bolt.new 4-8 hours Manual config Fair (framework-agnostic) Low
Cursor 1-3 weeks Full control Excellent High
v0 2-4 hours None (frontend only) Good (React) Low

Lovable won on speed because it connects directly to Supabase — a service that gives you a database, logins, and file storage out of the box. Cursor gives you more control and better code quality, but you still have to wire up everything yourself. Lovable does it all from a single prompt.

The real question: how much of that generated code is actually good enough for a real app?

The Solution

We built a customer portal in Lovable. Here’s the architecture it generated, with notes on what we kept and what we replaced.

Prompt to Production

The initial prompt was one paragraph:

Build a customer portal with:
- Email/password and Google OAuth login
- Project dashboard showing assigned projects with status
- File upload per project (PDF, images, max 10MB)
- Real-time activity feed showing project updates
- Stripe subscription management (view plan, update payment method)
- Role-based access: admin sees all projects, member sees assigned only
- Responsive design, dark mode toggle

Lovable generated the full stack in about 90 seconds. Here’s what it produced.

Generated Architecture

src/
  components/
    ui/              # shadcn/ui components (Button, Card, Dialog, etc.)
    auth/
      LoginForm.tsx
      SignupForm.tsx
      AuthGuard.tsx
    projects/
      ProjectList.tsx
      ProjectCard.tsx
      ProjectDetail.tsx
    files/
      FileUpload.tsx
      FileList.tsx
    feed/
      ActivityFeed.tsx
      FeedItem.tsx
    billing/
      SubscriptionCard.tsx
      PaymentMethodForm.tsx
  lib/
    supabase.ts       # Supabase client initialization
    utils.ts          # cn() helper, formatters
  hooks/
    useAuth.ts
    useProjects.ts
    useFiles.ts
    useActivityFeed.ts
    useSubscription.ts
  pages/
    Login.tsx
    Dashboard.tsx
    Project.tsx
    Settings.tsx
  App.tsx
  main.tsx

The Supabase Client (Generated, Kept)

Here’s what this piece does: This file connects your app to Supabase — the service that handles your database, user logins, and file storage. Think of it like plugging your phone into a charger: once it’s connected, everything else just works.

// src/lib/supabase.ts — Lovable generated this, we kept it as-is
import { createClient } from '@supabase/supabase-js';
import type { Database } from './database.types';

// These values come from your .env file (like a settings file for secrets)
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

// If the settings are missing, show a clear error instead of failing silently
if (!supabaseUrl || !supabaseAnonKey) {
  throw new Error(
    'Missing Supabase environment variables. ' +
    'Set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in your .env file.'
  );
}

// Create the connection — this is the "plug" that connects your app to Supabase
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
  auth: {
    persistSession: true,       // Keep the user logged in after page refresh
    autoRefreshToken: true,     // Automatically get a new token when the old one expires
    detectSessionInUrl: true,   // Handle OAuth logins that return via URL
  },
});

The Auth Hook (Generated, Modified)

Here’s what this piece does: This is the code that handles user logins, signups, and logouts. It’s like a bouncer at a club — it checks who you are and decides what you can see.

Lovable generated a basic auth hook. We added session refresh handling and error normalization (making error messages friendlier for users).

// src/hooks/useAuth.ts — generated, then modified
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';
import type { User, Session, AuthError } from '@supabase/supabase-js';

// This describes the shape of our auth data
interface AuthState {
  user: User | null;          // Who is logged in (null = no one)
  session: Session | null;   // Their login session
  loading: boolean;           // Are we still checking?
  error: AuthError | null;    // Any error that happened
}

export function useAuth() {
  const [state, setState] = useState<AuthState>({
    user: null,
    session: null,
    loading: true,   // Start in loading state — don't show anything until we know
    error: null,
  });

  useEffect(() => {
    // Step 1: Check if someone is already logged in (e.g., from a previous visit)
    supabase.auth.getSession().then(({ data: { session }, error }) => {
      setState({
        user: session?.user ?? null,
        session,
        loading: false,  // Done loading — now we know the user's status
        error,
      });
    });

    // Step 2: Listen for login/logout events (e.g., user clicks "Sign in with Google")
    const {
      data: { subscription },
    } = supabase.auth.onAuthStateChange((_event, session) => {
      setState((prev) => ({
        ...prev,
        user: session?.user ?? null,
        session,
        loading: false,
      }));
    });

    // Clean up: stop listening when this component is no longer on screen
    return () => subscription.unsubscribe();
  }, []);

  // Helper functions for common auth actions
  const signIn = async (email: string, password: string) => {
    const { error } = await supabase.auth.signInWithPassword({ email, password });
    if (error) throw normalizeAuthError(error);
  };

  const signInWithGoogle = async () => {
    const { error } = await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: { redirectTo: `${window.location.origin}/auth/callback` },
    });
    if (error) throw normalizeAuthError(error);
  };

  const signUp = async (email: string, password: string) => {
    const { error } = await supabase.auth.signUp({ email, password });
    if (error) throw normalizeAuthError(error);
  };

  const signOut = async () => {
    const { error } = await supabase.auth.signOut();
    if (error) throw normalizeAuthError(error);
  };

  return { ...state, signIn, signInWithGoogle, signUp, signOut };
}

// Turn confusing error messages into something users can understand
function normalizeAuthError(error: AuthError): Error {
  const messages: Record<string, string> = {
    'Invalid login credentials': 'Email or password is incorrect.',
    'Email not confirmed': 'Please check your inbox to confirm your email.',
    'User already registered': 'An account with this email already exists.',
  };
  return new Error(messages[error.message] || error.message);
}

The RLS Policies (Generated, Had to Fix)

Here’s what this piece does: RLS (Row-Level Security) policies are the rules that decide who can see what data. Think of them like a VIP list at a concert — some people can go anywhere, others can only go to certain areas.

This was the biggest gap. Lovable generated database tables and basic RLS policies, but they were too permissive. The generated policy for the projects table looked like this:

-- Generated by Lovable — too permissive
-- This says: "any logged-in user can see every project"
CREATE POLICY "Users can read projects" ON projects
  FOR SELECT USING (auth.role() = 'authenticated');

This lets any authenticated user see every project. We replaced it with proper role-based policies:

-- What we wrote — role-based access
-- Admins can see everything
CREATE POLICY "Admins can read all projects" ON projects
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM user_roles
      WHERE user_id = auth.uid()
      AND role = 'admin'
    )
  );

-- Regular members can only see projects assigned to them
CREATE POLICY "Members can read assigned projects" ON projects
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM project_members
      WHERE project_id = id
      AND user_id = auth.uid()
    )
  );

-- Only admins can create new projects
CREATE POLICY "Users can create projects" ON projects
  FOR INSERT WITH CHECK (
    EXISTS (
      SELECT 1 FROM user_roles
      WHERE user_id = auth.uid()
      AND role = 'admin'
    )
  );

Production pitfall: Lovable does not enable RLS by default on all tables. If you deploy without auditing every policy, you will leak data. Run SELECT * FROM pg_policies after every generation cycle and verify each one.

The Real-Time Feed (Generated, Kept)

Here’s what this piece does: This code creates a live-updating activity feed — like a Twitter feed for your project. When someone uploads a file or updates a task, it shows up instantly without refreshing the page.

The activity feed using Supabase real-time subscriptions worked out of the box.

// src/hooks/useActivityFeed.ts — kept as generated
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';
import type { RealtimePostgresChangesPayload } from '@supabase/supabase-js';

// Define what an activity looks like
interface Activity {
  id: string;
  project_id: string;
  user_id: string;
  action: string;          // What happened (e.g., "file_uploaded", "task_completed")
  description: string;     // Human-readable description
  created_at: string;      // When it happened
  user: { email: string; avatar_url: string | null };  // Who did it
}

export function useActivityFeed(projectId: string) {
  const [activities, setActivities] = useState<Activity[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Step 1: Load the last 50 activities from the database
    supabase
      .from('activities')
      .select('*, user:user_id(email, avatar_url)')  // Also get the user's info
      .eq('project_id', projectId)                    // Only for this project
      .order('created_at', { ascending: false })       // Newest first
      .limit(50)                                       // Max 50 items
      .then(({ data, error }) => {
        if (!error && data) setActivities(data as Activity[]);
        setLoading(false);
      });

    // Step 2: Subscribe to new activities as they happen (real-time)
    const channel = supabase
      .channel(`activities:${projectId}`)
      .on<Activity>(
        'postgres_changes',
        {
          event: 'INSERT',           // Only listen for new items
          schema: 'public',
          table: 'activities',
          filter: `project_id=eq.${projectId}`,  // Only for this project
        },
        (payload: RealtimePostgresChangesPayload<Activity>) => {
          // Add the new activity to the top of the list
          setActivities((prev) => [payload.new as Activity, ...prev]);
        }
      )
      .subscribe();

    // Clean up: unsubscribe when this component is no longer on screen
    return () => {
      supabase.removeChannel(channel);
    };
  }, [projectId]);

  return { activities, loading };
}

How to Use Effectively

Lovable is not a code editor — it’s a product generator. Think of it like a 3D printer for web apps: you describe what you want, and it builds the whole thing. The trick is knowing how to describe things clearly.

Step-by-Step for Beginners

Step 1: Write a clear prompt. Structure every prompt like this:

[Context]     — Who the users are, what problem this solves
[Features]    — Bullet list of capabilities, ordered by priority
[Constraints] — Tech stack, design system, performance requirements

Here’s an example:

Context: Internal tool for a 50-person design agency. Users are non-technical
designers who need to track project feedback.

Features:
- Upload design files (Figma exports, PDFs, images)
- Annotate files with comments pinned to coordinates
- Threaded replies on each annotation
- Email notifications when someone replies to your comment
- Export all comments as CSV

Constraints:
- Must use shadcn/ui components
- Dark mode by default
- Files under 15MB
- No Stripe — flat monthly billing via invoice

Step 2: Review what it built. Don’t just look at the preview — read the code. Check that the database tables make sense and the security rules are correct.

Step 3: Fix the security rules. This is the most important step. Lovable’s default security is too loose. You need to tighten it for every table.

Step 4: Test the edge cases. What happens when there’s no data? What happens when a user tries to access something they shouldn’t? What happens when the internet goes down?

Step 5: Save your work. Push to GitHub before making the next change. This way you can always go back if something breaks.

The Iteration Loop

  1. Prompt — Describe the feature in one sentence
  2. Review — Check the generated code, not just the preview
  3. Audit RLS — Verify every new table has correct policies
  4. Test edge cases — Empty states, error states, permission denials
  5. Commit — Push to GitHub before the next prompt

Core lesson: Never prompt for the next feature until you have audited the current one. Lovable’s context window is limited — once you have 15-20 screens, the model starts hallucinating existing components and breaking working features.

Use Cases

1. Customer-Facing Portal (What We Built)

When you’d use this: You need to give your customers a place to log in, see their projects, upload files, and manage their billing.

Why this tool fits: Lovable generated the full stack in 90 seconds. We spent 4 hours hardening security rules, adding error handling, and writing tests. Total time to production: one day instead of three weeks.

2. Internal Admin Dashboard

When you’d use this: Your team needs a dashboard to manage users, view analytics, and send emails — and the deadline is this Friday.

Why this tool fits: Lovable generated the data entry screens, database tables, and basic charts. The team kept 80% of the generated code and only rewrote the analytics queries for speed.

3. SaaS Landing Page with Waitlist

When you’d use this: You’re launching a product and need a landing page with email signups, referral tracking, and a launch countdown — fast.

Why this tool fits: Lovable generated the page, database table for signups, and a simple referral system in under an hour. The generated code was production-ready with minimal changes.

4. Marketplace Prototype

When you’d use this: You want to test a two-sided marketplace idea — freelancers connecting with clients — before investing in a full build.

Why this tool fits: Lovable generated user profiles, listing creation, search, and messaging. The messaging (real-time chat) worked immediately. The search needed a rewrite — Lovable generated client-side filtering, which does not scale past 100 listings.

5. Internal Tool for Operations

When you’d use this: A logistics company needs a tool to track shipments, assign drivers, and generate delivery manifests.

Why this tool fits: Lovable generated the full data entry interface and database backend. The team kept 90% of the code and only rewrote the PDF generation and route optimization logic.

Cheat Sheet

Aspect Details
Platform lovable.dev — browser-based, no install
Generated Stack React + TypeScript + Vite + Tailwind + shadcn/ui + Supabase
Pricing Free (30 credits/mo), Pro 100 ($25/mo), Pro 1200 ($294/mo), Business ($50/mo)
Free Tier Limited free tier (varies) — great for prototyping and learning
Credit Burn Rate ~1 credit per prompt; complex prompts may cost 2-3 credits
Deployment One-click to Lovable Cloud (built-in Supabase) or custom domain
GitHub Sync Full repo sync — code is yours, not locked in
Backend Options Lovable Cloud (default) or self-managed Supabase
Auth Providers Email/password, Google, GitHub, Apple, Facebook, magic link
File Storage Up to 2GB per file, private buckets by default
Real-Time Supabase Realtime — works out of the box for subscriptions
RLS Default Not enabled on all tables — must audit every policy
Context Limit ~15-25 screens before coherence degrades
Max File Upload 2GB per file (Supabase limit)
Custom Domain Pro plan and above
Export Full GitHub repo — no vendor lock on code
Common Gotcha Lovable generates fetch() calls to endpoints that may not exist
Debugging “Try to fix” button auto-resolves build errors; browser console for runtime
Security ISO 27001, SOC 2 Type II, GDPR compliant platform

Vibe Coding Projects

Project 1: Personal CRM

What it does: Track interactions with your network — log meetings, set follow-up reminders, view a relationship dashboard with engagement scores.

What you’ll learn: Prompting for relational data models, real-time subscriptions, and auth patterns. You will also learn to audit RLS policies — the generated defaults will let every user see every contact.

Estimated effort: 2-3 hours to generate, 1-2 hours to harden.

Project 2: Team Retrospective Board

What it does: A real-time board where team members post what went well, what went wrong, and action items. Vote on items, assign owners, and track resolution status across sprints.

What you’ll learn: Real-time multi-user collaboration patterns, row-level security for team-scoped data, and the limits of AI-generated state management.

Estimated effort: 3-4 hours to generate, 2-3 hours to add voting logic and data export.

Project 3: Invoice Generator with Stripe

What it does: Create and send invoices, track payment status, generate PDF receipts, and sync with Stripe for payment collection. Dashboard shows revenue, outstanding invoices, and payment history.

What you’ll learn: Stripe integration patterns, edge functions for payment processing, and the gap between generated code and production-ready payment handling.

Estimated effort: 4-5 hours to generate, 3-4 hours to add Stripe webhook handling and PDF generation.

Problems Solved Efficiently

Problem Type Why Lovable Fits When to Look Elsewhere
CRUD-heavy apps with auth Generates 80% of forms, tables, and login code correctly on first try Your app has complex custom business logic that doesn’t fit standard patterns
Internal tools with tight deadlines Delivers a working, deployed app in hours — stakeholders see progress immediately You need enterprise-grade security and compliance from day one
Prototypes that need real URLs Generates a deployed app with a real database and real auth — not just a mockup You’re building a consumer app that needs to scale to millions of users
Single-page SaaS apps 5-10 screens with standard features is Lovable’s sweet spot — code is clean enough to maintain for months Your app has 50+ screens with complex state management
Non-technical founders validating ideas Fastest path to a working demo — generated UI looks professional out of the box You need full control over every line of code for a long-term product

The Results

We shipped the customer portal in 6 hours total — 90 seconds of generation time, 4 hours of hardening, 1.5 hours of testing and deployment.

Metric Traditional Build Lovable Build Improvement
Time to deployed MVP 3-4 weeks 6 hours ~95% faster
Lines of code written ~8,000 ~200 (modifications) 97.5% less
Auth flows 2 days to wire up Generated in 90s Instant
Database schema + RLS 3 days 90s generated + 2h audit ~90% faster
File upload + storage 1 day Generated in 90s Instant
Real-time feed 1 day Generated in 90s Instant
Stripe integration 2 days Generated in 90s + 1h webhook config ~90% faster
Code we kept unchanged ~65%
Code we modified ~25%
Code we rewrote entirely ~10%

What this means for you: If you’re building a standard web app with logins, data entry, and file uploads, Lovable can save you weeks of work. About 65% of the generated code is production-ready as-is. The parts you’ll need to fix are mostly security rules and error handling. The key is knowing what to check before you deploy.

What to Watch Out For

What We Sacrificed

1. RLS policies are too permissive by default. Lovable generates auth.role() = 'authenticated' as the default policy for every table. This means any logged-in user can read any row in your database. We caught this during audit, but it would have been a data breach in production. Fix: Run SELECT * FROM pg_policies after every generation and verify each policy against your access model. Think of it like checking every door in a building to make sure the locks actually work.

2. The context window breaks around 15-25 screens. As your app grows, Lovable starts making things up. It will reference components that don’t exist, create duplicate files, and break working features when adding new ones. Fix: Export to GitHub early and switch to Cursor for the maintenance phase. Lovable is a scaffolding tool — great for the first draft, not for long-term development. Think of it like using a power saw to cut lumber, then switching to a hand plane for the fine details.

3. Generated error handling is minimal. Lovable generates basic error catching but rarely adds user-friendly error messages, retry logic, or offline state handling. Every generated hook needs a pass to add proper error boundaries and loading states. Fix: Wrap all generated data-fetching hooks in a generic error boundary component. Your users should never see a blank screen or a cryptic error message.

Three Things That Went Wrong

1. The Stripe webhook was not idempotent. Lovable generated a Stripe integration that processed webhooks without checking for duplicate events. In testing, a single subscription update fired three webhooks, creating three duplicate invoice records. We rewrote the edge function to use Stripe’s idempotency_key. Beginner tip: “Idempotent” just means “running the same thing twice gives the same result.” Always check for duplicates when processing payment events.

2. File uploads had no type validation. The generated file upload accepted any file type and any size. A tester uploaded a 200MB video file, which crashed the Supabase storage bucket and consumed the project’s storage quota. We added client-side and server-side validation for file type and size. Beginner tip: Always limit what files users can upload — both the type (PDF, images only) and the size (max 10MB). Do this check on the user’s browser AND on your server.

3. Search was client-side only. Lovable generated a search input that filtered results with JavaScript .filter(). This works for 50 records but breaks at 500. We rewrote it as a Supabase query with ilike pattern matching and a database index on the search column. Beginner tip: Client-side filtering means loading ALL data into the browser and searching there. That’s fine for tiny datasets. For anything bigger, let the database do the searching — it’s built for it.

Advice for Beginners

Use Lovable for what it is: the fastest way to build a first version of your app. Describe your app, get a working full-stack codebase in 90 seconds, then immediately export to GitHub and audit every line. Don’t spend more than 2-3 days inside Lovable — the quality degrades as your app grows.

The ideal workflow: Lovable for the first 80% (scaffolding, auth, data entry, deployment), then Cursor for the last 20% (hardening, custom logic, performance optimization). The combination of both tools beats either one alone.

Final lesson: Lovable generates code that looks right. It does not generate code that is secure, performant, or maintainable. Those are your job. Treat every generated line as a first draft that needs review, not a finished product.

Course-Style Deep Dive

How Lovable Works Under the Hood (Simplified)

Think of Lovable as a factory with multiple robots working together. You give it a description of what you want, and each robot handles a different part of the job.

The Five-Step Process:

  1. Spec — Your prompt enters the system. Lovable reads it, figures out what you’re asking for (a login page? a file upload? a payment system?), and builds a plan. Think of this like a chef reading a recipe and gathering ingredients.

  2. Context — Lovable looks at your current project: what files exist, what components you already have, what’s in your database. This is the bottleneck — it can only hold so much information at once, which is why apps degrade past 15-25 screens. Think of it like a desk that can only fit so many papers.

  3. Plan — The AI (Claude or GPT, depending on your plan) decides exactly what to build:

    • Frontend: new components, pages, data-fetching hooks
    • Backend: new database tables, security rules, server functions
    • Data: migration steps, sample data
    • Integration: API endpoints to call, webhooks to register
  4. Build & Preview — Lovable writes the code, compiles it (checks for errors), and shows you a live preview. If the build fails, the “Try to fix” button feeds the error back into the system for automatic fixing.

  5. Feedback — Your next instruction, or the error from a failed build, goes back to step 1. Each cycle costs 1-3 credits regardless of whether it succeeds.

The Supabase Connection:

The key to Lovable’s power is its direct connection to Supabase. Think of this like a universal remote that can control your TV, sound system, and lights all at once. Lovable can:

  • Read: See your database structure, existing security rules, how many rows are in each table
  • Write: Create tables, add columns, add indexes, update security rules, deploy server functions
  • Execute: Run database migrations, trigger server functions, manage file storage

This is why Lovable can generate a full backend from a single prompt — it’s not guessing the schema, it’s creating it in real time through this direct connection.

Advanced Patterns: Beyond the Basics

Pattern 1: Multi-Tenant Architecture

Lovable’s default generates single-tenant apps (one company, one set of data). To build multi-tenant (many companies, each with their own data):

-- Add organization_id to every table
ALTER TABLE projects ADD COLUMN organization_id UUID REFERENCES organizations(id);
ALTER TABLE files ADD COLUMN organization_id UUID REFERENCES organizations(id);

-- RLS policy scoped to organization
CREATE POLICY "Users can read org projects" ON projects
  FOR SELECT USING (
    organization_id IN (
      SELECT organization_id FROM organization_members
      WHERE user_id = auth.uid()
    )
  );

Prompt this as: “Add organization_id to all tables and scope RLS policies by organization membership.”

Pattern 2: Soft Deletes

Lovable does not generate soft deletes by default. Soft deletes mean you mark an item as deleted instead of actually removing it — like putting a file in the trash instead of shredding it.

ALTER TABLE projects ADD COLUMN deleted_at TIMESTAMPTZ DEFAULT NULL;

CREATE INDEX idx_projects_active ON projects (organization_id)
  WHERE deleted_at IS NULL;

Then update all SELECT queries to filter IS NULL deleted_at. Prompt: “Add soft delete to all tables with deleted_at column and update all queries to filter out deleted records.”

Pattern 3: Audit Logging

Every data change should be logged. Lovable does not generate audit trails:

CREATE TABLE audit_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES auth.users(id),
  action TEXT NOT NULL,        — 'INSERT', 'UPDATE', 'DELETE'
  table_name TEXT NOT NULL,
  record_id UUID NOT NULL,
  old_data JSONB,
  new_data JSONB,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Trigger function for any table
CREATE OR REPLACE FUNCTION log_audit()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO audit_log (user_id, action, table_name, record_id, old_data, new_data)
  VALUES (
    auth.uid(),
    TG_OP,
    TG_TABLE_NAME,
    COALESCE(NEW.id, OLD.id),
    CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN row_to_json(OLD)::jsonb ELSE NULL END,
    CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW)::jsonb ELSE NULL END
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Production Considerations

Monitoring and Error Tracking

Lovable does not generate any monitoring code. Add these after every generation:

// src/lib/error-tracking.ts — add manually
import * as Sentry from '@sentry/react';

export function initErrorTracking() {
  Sentry.init({
    dsn: import.meta.env.VITE_SENTRY_DSN,
    environment: import.meta.env.MODE,
    tracesSampleRate: 0.2,
    integrations: [Sentry.browserTracingIntegration()],
  });
}

// Wrap generated data hooks with error reporting
export async function withErrorReporting<T>(
  operation: string,
  fn: () => Promise<T>
): Promise<T> {
  try {
    return await fn();
  } catch (error) {
    Sentry.captureException(error, {
      extra: { operation },
    });
    throw error;
  }
}

Rate Limiting and Retries

Supabase has rate limits (default: 30 requests per second per project). Lovable does not generate any retry logic:

// src/lib/retry.ts — add manually
export async function withRetry<T>(
  fn: () => Promise<T>,
  options: { maxRetries?: number; baseDelay?: number } = {}
): Promise<T> {
  const { maxRetries = 3, baseDelay = 1000 } = options;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries) throw error;

      // Check if retryable (429 or 5xx)
      if (error instanceof Error && 'status' in error) {
        const status = (error as any).status;
        if (status === 429 || status >= 500) {
          const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
          await new Promise((resolve) => setTimeout(resolve, delay));
          continue;
        }
      }
      throw error; // Non-retryable error
    }
  }
}

Database Connection Pooling

Supabase projects have a default max of 15 connections. Lovable generates direct client connections without pooling. For production:

// src/lib/supabase.ts — production version with connection management
import { createClient, SupabaseClient } from '@supabase/supabase-js';

let client: SupabaseClient | null = null;

export function getSupabaseClient(): SupabaseClient {
  if (!client) {
    client = createClient(
      import.meta.env.VITE_SUPABASE_URL,
      import.meta.env.VITE_SUPABASE_ANON_KEY,
      {
        auth: { persistSession: true, autoRefreshToken: true },
        realtime: { params: { eventsPerSecond: 10 } },
        global: {
          headers: { 'X-Client-Info': 'nivant-labs-portal' },
        },
      }
    );
  }
  return client;
}

Integration Patterns

Lovable + Cursor (The Hybrid Workflow)

This is the most effective pattern we have found:

  1. Day 1: Generate the full app in Lovable. Deploy to Lovable Cloud. Show the stakeholder.
  2. Day 2: Export to GitHub. Clone locally. Open in Cursor.
  3. Day 3-4: Use Cursor’s Agent mode to audit RLS policies, add error boundaries, implement retry logic, and harden the Stripe integration.
  4. Day 5: Deploy to production (Vercel or Cloudflare) with the hardened Supabase project.

Lovable + n8n (Workflow Automation)

Lovable integrates with 400+ tools through n8n. For apps that need email campaigns, Slack notifications, or CRM sync:

  1. Generate the app in Lovable with Supabase backend
  2. Connect n8n to the same Supabase database
  3. Build workflows that trigger on database changes (new user signup, project created, payment received)
  4. n8n handles the integration logic while Lovable handles the UI

Lovable + Stripe (Payment Processing)

The generated Stripe integration needs significant hardening:

// src/lib/stripe.ts — rewritten from generated code
import Stripe from 'stripe';

const stripe = new Stripe(import.meta.env.VITE_STRIPE_SECRET_KEY, {
  apiVersion: '2025-09-01',
  maxNetworkRetries: 3,
});

// Edge function: create checkout session
export async function createCheckoutSession(
  customerId: string,
  priceId: string
): Promise<Stripe.Checkout.Session> {
  return stripe.checkout.sessions.create({
    customer: customerId,
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${import.meta.env.VITE_APP_URL}/billing?success=true`,
    cancel_url: `${import.meta.env.VITE_APP_URL}/billing?canceled=true`,
    metadata: { source: 'lovable-portal' },
  });
}

// Edge function: handle webhook with idempotency
export async function handleStripeWebhook(
  body: string,
  signature: string
): Promise<{ received: boolean }> {
  const event = stripe.webhooks.constructEvent(
    body,
    signature,
    import.meta.env.VITE_STRIPE_WEBHOOK_SECRET
  );

  // Check idempotency — Lovable did not generate this
  const { data: existing } = await supabase
    .from('stripe_events')
    .select('id')
    .eq('stripe_event_id', event.id)
    .single();

  if (existing) {
    return { received: true }; // Already processed
  }

  // Process the event
  switch (event.type) {
    case 'invoice.payment_succeeded':
      await handlePaymentSucceeded(event.data.object);
      break;
    case 'customer.subscription.updated':
      await handleSubscriptionUpdated(event.data.object);
      break;
    case 'customer.subscription.deleted':
      await handleSubscriptionDeleted(event.data.object);
      break;
  }

  // Record for idempotency
  await supabase.from('stripe_events').insert({
    stripe_event_id: event.id,
    type: event.type,
    processed_at: new Date().toISOString(),
  });

  return { received: true };
}

The generated code will call Stripe APIs but will not handle idempotency, webhook signature verification, or event deduplication. Every one of these is required for production payment processing.

NL

Written by Nivant Labs Team

Engineer at Nivant Labs

Share this post