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_policiesafter 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
- Prompt — Describe the feature in one sentence
- Review — Check the generated code, not just the preview
- Audit RLS — Verify every new table has correct policies
- Test edge cases — Empty states, error states, permission denials
- 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:
-
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.
-
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.
-
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
-
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.
-
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:
- Day 1: Generate the full app in Lovable. Deploy to Lovable Cloud. Show the stakeholder.
- Day 2: Export to GitHub. Clone locally. Open in Cursor.
- Day 3-4: Use Cursor’s Agent mode to audit RLS policies, add error boundaries, implement retry logic, and harden the Stripe integration.
- 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:
- Generate the app in Lovable with Supabase backend
- Connect n8n to the same Supabase database
- Build workflows that trigger on database changes (new user signup, project created, payment received)
- 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.
Written by Nivant Labs Team
Engineer at Nivant Labs