security

Lovable App Security Checklist: 10 Things to Fix Before Launch

Lovable builds beautiful apps fast. But a May 2025 audit found 10% of Lovable apps had security vulnerabilities exposing user data. Here are the 10 most common issues and how to fix each one.

FinishKit Team11 min read

Lovable is genuinely impressive. You describe an app, and minutes later you have a working product with authentication, a database, and polished UI. It's changed what's possible for builders who don't have years of engineering experience.

But there's a problem. In May 2025, a security researcher audited 1,645 Lovable-created web applications and found that 170 of them had security vulnerabilities that exposed personal user data to anyone with a browser and basic knowledge of developer tools. That's roughly 10% of all apps examined.

The vulnerabilities weren't sophisticated. They were basic, well-known security gaps that Lovable's code generation simply doesn't address by default. An exposed Supabase key here. A missing RLS policy there. Authorization logic that only exists in the frontend.

This isn't about blaming Lovable. It's about understanding that code generation tools optimize for making things work, not for making them secure. Security requires a different kind of attention, and right now, that attention falls on you.

Here are the 10 most common security issues in Lovable apps and exactly how to fix each one.

1. Missing Row-Level Security (RLS) Policies

The problem: Lovable creates Supabase tables for your data but often doesn't enable Row-Level Security. Without RLS, your Supabase anon key (which is public and visible in your frontend code) grants full read/write access to every row in every table.

The risk: Anyone can open their browser console and run queries against your database. They can read every user's data, modify records, or delete your entire dataset.

The fix:

-- Check which tables are missing RLS
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND rowsecurity = false;
 
-- Enable RLS and add policies for each table
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
 
-- Users can only read their own profile
CREATE POLICY "read_own_profile" ON public.user_profiles
  FOR SELECT USING (auth.uid() = user_id);
 
-- Users can only update their own profile
CREATE POLICY "update_own_profile" ON public.user_profiles
  FOR UPDATE USING (auth.uid() = user_id);

This is the single most critical security issue in Lovable apps. If you do nothing else on this list, do this. Every table with user data must have RLS enabled with appropriate policies.

2. Exposed Supabase Service Role Key

The problem: Lovable sometimes places the Supabase service role key in client-side code or in a .env file that gets bundled into the frontend build. The service role key bypasses all RLS policies, meaning anyone who finds it has unrestricted access to your entire database.

The risk: Complete database compromise. The service role key is equivalent to admin access with no restrictions.

The fix:

# Search for the service role key in your codebase
grep -r "service_role" src/ --include="*.ts" --include="*.tsx" --include="*.js"
grep -r "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" src/ --include="*.ts" --include="*.tsx"

If you find it anywhere in your frontend code:

  1. Remove it immediately
  2. Rotate the key in your Supabase dashboard (Settings > API)
  3. Only use the service role key in server-side code (Edge Functions, API routes)
  4. Use the anon key for all client-side operations
// WRONG: Service role key in client code
const supabase = createClient(url, 'eyJhbGci...service_role_key');
 
// RIGHT: Anon key in client code, service role only on server
const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY  // This is the public anon key
);

3. Client-Side Authorization Logic

The problem: Lovable often implements authorization checks purely in the frontend. The UI hides admin buttons or restricts navigation based on a user's role, but the underlying API calls and database operations are still accessible to anyone.

The risk: Any user can bypass frontend restrictions by calling the API directly from browser dev tools or a script.

The fix: Authorization must be enforced at the database level (via RLS) or in server-side code:

-- RLS policy that checks user role
CREATE POLICY "admin_only_delete" ON public.content
  FOR DELETE USING (
    EXISTS (
      SELECT 1 FROM public.profiles
      WHERE profiles.id = auth.uid()
      AND profiles.role = 'admin'
    )
  );

For more complex authorization, use Supabase Edge Functions:

// supabase/functions/admin-action/index.ts
Deno.serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  );
 
  const token = req.headers.get('Authorization')?.replace('Bearer ', '');
  const { data: { user } } = await supabase.auth.getUser(token!);
 
  // Server-side role check - cannot be bypassed
  const { data: profile } = await supabase
    .from('profiles')
    .select('role')
    .eq('id', user!.id)
    .single();
 
  if (profile?.role !== 'admin') {
    return new Response('Forbidden', { status: 403 });
  }
 
  // Proceed with admin action...
});

4. No Rate Limiting on Authentication

The problem: Lovable apps typically have no rate limiting on login attempts, password resets, or signup flows. This makes them vulnerable to brute-force attacks and abuse.

The risk: An attacker can attempt thousands of passwords per minute against any user account, or abuse your signup flow to create spam accounts.

The fix: Supabase has built-in rate limiting for auth endpoints, but you should verify it's configured and add application-level limiting:

// Simple in-memory rate limiter for Edge Functions
const attempts = new Map<string, { count: number; resetAt: number }>();
 
function rateLimit(identifier: string, maxAttempts = 5, windowMs = 900000) {
  const now = Date.now();
  const record = attempts.get(identifier);
 
  if (!record || now > record.resetAt) {
    attempts.set(identifier, { count: 1, resetAt: now + windowMs });
    return { allowed: true, remaining: maxAttempts - 1 };
  }
 
  if (record.count >= maxAttempts) {
    return { allowed: false, remaining: 0, retryAfter: record.resetAt - now };
  }
 
  record.count++;
  return { allowed: true, remaining: maxAttempts - record.count };
}

Also configure Supabase's built-in rate limiting in your dashboard under Authentication > Rate Limits.

5. Missing Input Validation

The problem: Lovable generates forms that send user input directly to the database without validation or sanitization. Whatever the user types goes straight into Supabase.

The risk: Malformed data, injection attacks, and unexpected application behavior. Users can submit empty strings, extremely long values, or specially crafted input that breaks your app.

The fix: Validate all input before it touches your database:

import { z } from 'zod';
 
const CreateProjectSchema = z.object({
  name: z.string()
    .min(1, 'Project name is required')
    .max(100, 'Project name must be under 100 characters')
    .trim(),
  description: z.string()
    .max(1000, 'Description must be under 1000 characters')
    .optional(),
  url: z.string()
    .url('Must be a valid URL')
    .optional(),
});
 
// In your form handler
function handleSubmit(formData: unknown) {
  const result = CreateProjectSchema.safeParse(formData);
 
  if (!result.success) {
    // Show validation errors to user
    setErrors(result.error.flatten().fieldErrors);
    return;
  }
 
  // Safe to send to database
  await supabase.from('projects').insert(result.data);
}

6. Permissive CORS Configuration

The problem: Lovable apps sometimes configure overly permissive CORS headers, allowing any website to make API requests to your backend.

The risk: Other websites can make authenticated requests to your API on behalf of your users, potentially reading or modifying their data.

The fix: Restrict CORS to your own domains:

// In your Edge Function or API route
const ALLOWED_ORIGINS = [
  'https://your-app.com',
  'https://www.your-app.com',
];
 
const origin = req.headers.get('Origin');
const corsHeaders: Record<string, string> = {};
 
if (origin && ALLOWED_ORIGINS.includes(origin)) {
  corsHeaders['Access-Control-Allow-Origin'] = origin;
  corsHeaders['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE';
  corsHeaders['Access-Control-Allow-Headers'] = 'Authorization, Content-Type';
}
 
// Don't use Access-Control-Allow-Origin: '*' for authenticated endpoints

7. No Security Headers

The problem: Lovable's generated apps don't include security headers, leaving users vulnerable to clickjacking, XSS, and other browser-based attacks.

The risk: Your app can be embedded in malicious iframes, scripts can be injected into your pages, and browsers won't enforce basic security protections.

The fix: Add security headers to your deployment configuration. For Vercel:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains" },
        { "key": "Content-Security-Policy", "value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co" }
      ]
    }
  ]
}

Content-Security-Policy requires careful tuning for your specific app. Start with a restrictive policy and loosen it as needed. A misconfigured CSP can break your app, so test thoroughly.

8. Exposed Storage Buckets

The problem: Lovable apps that handle file uploads often create Supabase Storage buckets without proper access policies. This means uploaded files (user avatars, documents, attachments) may be publicly accessible to anyone who can guess or enumerate the file URLs.

The risk: Private user files can be accessed by anyone. If users upload sensitive documents, they're effectively public.

The fix:

-- Check your storage bucket policies
SELECT * FROM storage.buckets;
 
-- Create a private bucket (if not already)
INSERT INTO storage.buckets (id, name, public)
VALUES ('user-documents', 'user-documents', false);
 
-- Add RLS policies to the storage.objects table
CREATE POLICY "Users can upload to own folder"
  ON storage.objects FOR INSERT
  WITH CHECK (
    bucket_id = 'user-documents'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );
 
CREATE POLICY "Users can view own files"
  ON storage.objects FOR SELECT
  USING (
    bucket_id = 'user-documents'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

9. No Error Boundaries or Error Handling

The problem: Lovable generates apps with minimal error handling. When an API call fails, the app either shows a blank screen or exposes the raw error message to the user, potentially leaking implementation details.

The risk: Unhandled errors can expose database schema information, API keys in error messages, and internal server details. They also create a terrible user experience.

The fix:

// Add an error boundary to your app root
import { Component, type ReactNode } from 'react';
 
class ErrorBoundary extends Component<
  { children: ReactNode; fallback: ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };
 
  static getDerivedStateFromError() {
    return { hasError: true };
  }
 
  componentDidCatch(error: Error) {
    // Log to your error tracking service, not to the console
    console.error('Application error:', error.message);
  }
 
  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}
 
// Wrap API calls with proper error handling
async function fetchData() {
  try {
    const { data, error } = await supabase.from('items').select('*');
    if (error) throw error;
    return data;
  } catch (err) {
    // Show user-friendly message, not raw error
    toast.error('Failed to load items. Please try again.');
    return [];
  }
}

10. Missing Logout and Session Management

The problem: Lovable apps sometimes implement authentication without proper session management. Sessions don't expire, logout doesn't fully clear state, and there's no handling for expired or revoked tokens.

The risk: Stale sessions on shared computers, tokens that remain valid after the user expects to be logged out, and no protection against session hijacking.

The fix:

// Proper logout function
async function handleLogout() {
  // Sign out from Supabase (invalidates the session)
  await supabase.auth.signOut();
 
  // Clear any local state
  localStorage.removeItem('app-state');
  sessionStorage.clear();
 
  // Redirect to login page
  window.location.href = '/login';
}
 
// Listen for auth state changes to handle expired sessions
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_OUT' || event === 'TOKEN_REFRESHED') {
    if (!session) {
      // Session expired or was revoked - redirect to login
      window.location.href = '/login';
    }
  }
});

Configure session timeouts in Supabase dashboard under Authentication > Settings. For most apps, a 1-hour JWT expiry with automatic refresh is a good default.

The Bigger Picture

These 10 issues aren't bugs in Lovable. They're gaps in what any code generation tool covers. Lovable optimizes for getting you a working app fast, and it does that better than almost anything else on the market. But "working" and "secure" are different things, and right now, the gap between them is your responsibility to close.

If you're using Lovable to build something that handles real user data, real payments, or real business logic, you need to treat the generated code as a starting point, not a finished product. The comparison between AI coding tools covers this pattern in more depth: every build tool has blind spots, and security is consistently the biggest one.

Don't want to audit your Lovable app manually? FinishKit scans your repo and generates a prioritized Finish Plan that catches security gaps, missing error handling, and production readiness issues. It takes about 2 minutes and tells you exactly what needs fixing before launch.

Lovable built it. You need to finish it. These 10 fixes are where to start.