• Agents
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Guides
  • Blog
  • Docs
  • OWASP Top 10
  • Glossary
  • FAQ

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Back to Blog

Mar 28, 2026 · 14 min read

How to Secure Your Lovable App Before You Launch

A step-by-step security guide for apps built with Lovable. Fix the most common vulnerabilities in under an hour — no security expertise required.

FFlowpatrol Team·Guides
How to Secure Your Lovable App Before You Launch

You shipped it. Now lock it down.

In May 2025, Matt Palmer disclosed CVE-2025-48757: 170+ Lovable-built apps had Row Level Security disabled, exposing 303 database endpoints to unauthenticated reads. Personal debt records. Home addresses. API keys. All readable with a single curl command using the anon key sitting in every Lovable app's HTML source.

None of those builders did anything wrong on purpose. Lovable's AI creates Supabase tables through the chat flow and doesn't enable RLS. It wires the anon key into the client bundle and calls it done. The app works. The defaults look fine. They aren't.

This checklist fixes that. Seven steps, real commands, about an hour. Work through it top to bottom and you'll ship something that won't end up in a disclosure report.


Why Lovable apps keep showing up in disclosures

Every Lovable app ships on the same stack: React + Vite frontend, Supabase backend, deployed to yourproject.lovable.app or a custom domain. The stack is solid. The defaults are the problem.

Five things happen on almost every Lovable project:

  1. The AI creates Supabase tables through the chat flow and doesn't enable RLS unless you explicitly ask for it.
  2. The anon key and project URL land in the client bundle (fine — that's what the anon key is for) but so does anything else you pasted into the chat, including keys you intended to keep private.
  3. When RLS is added, the AI frequently writes USING (true) as a placeholder — functionally identical to no policy at all.
  4. Edge Functions are generated with verify_jwt = false in config.toml, meaning anyone on the internet can call them without a session.
  5. Server-side code that uses the service role key bypasses RLS entirely — and the ownership check that should replace it never gets written.

Across Lovable apps we've scanned, almost every critical finding traces back to one of those five patterns.


Step 1: Enable Row Level Security on every table

Time: 10 minutes | Impact: Critical

This is the single most important step. For a deep dive on how RLS works and why AI skips it, see our full RLS guide. Open your Supabase dashboard, go to the SQL Editor, and run this to see where you stand:

SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;

Every row where rowsecurity is false is a table anyone on the internet can read with just your anon key. That includes the profiles table Lovable auto-creates, any user_* tables, and the "example" tables the AI adds during prompting that you forgot to delete.

The Lovable gotcha to watch for: when you ask the AI to "add RLS," it will often generate a policy like USING (true) as a placeholder, which is functionally identical to no RLS at all. Run this to find those:

SELECT schemaname, tablename, policyname, qual
FROM pg_policies
WHERE schemaname = 'public'
  AND (qual = 'true' OR qual IS NULL);

Any row that comes back is a policy that lets anyone read everything. Rewrite those using the examples below.

Security guide progress checklist showing step one complete
Security guide progress checklist showing step one complete

Enable RLS

-- Replace with your actual table names
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE items ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
-- Repeat for EVERY table

Create basic policies

For most tables in a Lovable app, you want "users can only access their own data." The user ID column is typically user_id or created_by:

-- Profiles: users read and update their own profile
CREATE POLICY "Users manage own profile"
ON profiles FOR ALL
USING (auth.uid() = id);

-- User-owned data: full CRUD on own rows
CREATE POLICY "Users manage own items"
ON items FOR ALL
USING (auth.uid() = user_id);

-- For INSERT, also restrict which user_id can be set
CREATE POLICY "Users create own items"
ON items FOR INSERT
WITH CHECK (auth.uid() = user_id);

If your app has shared or public data (like published listings), add a separate read policy:

CREATE POLICY "Anyone can view published listings"
ON listings FOR SELECT
USING (status = 'published');

Verify it worked

Open an incognito browser window, go to your app, and check that unauthenticated users can't see data they shouldn't. You can also test directly:

curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"

This should return an empty array or an error — not your users' data.


Step 2: Find leaked secrets in your bundle

Time: 10 minutes | Impact: Critical

Lovable is a Vite app. Anything Vite ships to the browser is readable by anyone who opens DevTools. If you ever told the AI "use my OpenAI key, it's sk-..." or pasted a Stripe secret into the chat, there's a real chance that key is sitting in your production bundle right now. For the full breakdown, see Your .env Is Showing.

Check what's in your bundle

  1. Open your deployed app (yourproject.lovable.app or your custom domain) in Chrome
  2. Open DevTools (F12) → Sources tab → expand the top-level folder
  3. Search across all files (Ctrl+Shift+F) for these patterns:
    • sk- (OpenAI / Anthropic keys)
    • sk_live or rk_live (Stripe secret / restricted keys)
    • service_role
    • SUPABASE_SERVICE or SERVICE_ROLE
    • secret or SECRET
    • xoxb- (Slack bot tokens)
    • AKIA (AWS access keys)

Or run a one-liner from your terminal that fetches the deployed JS and greps for common key prefixes:

curl -s https://yourproject.lovable.app/ \
  | grep -oE '"/assets/[^"]+\.js"' \
  | tr -d '"' \
  | xargs -I{} curl -s "https://yourproject.lovable.app{}" \
  | grep -oE '(sk-[a-zA-Z0-9]{20,}|sk_live_[a-zA-Z0-9]{20,}|service_role)' \
  | sort -u

If that command prints anything, stop and rotate those keys before continuing.

Safe to find in client code:

  • Supabase anon key (starts with eyJ...) — safe IF RLS is enabled
  • Supabase project URL
  • Stripe publishable key (pk_live_...)

Must NOT be in client code:

  • OpenAI/Anthropic API keys
  • Stripe secret key
  • Supabase service role key
  • Any password or secret token

If you find exposed secrets

  1. Rotate them immediately. Generate new keys from the provider's dashboard. The old ones are compromised.
  2. Move them server-side. Use Supabase Edge Functions or Next.js API routes to keep secrets on the server.

Example — moving an OpenAI call server-side with a Supabase Edge Function:

// supabase/functions/ai-chat/index.ts
import { serve } from "https://deno.land/std/http/server.ts";

serve(async (req) => {
  const { message } = await req.json();

  const response = await fetch(
    "https://api.openai.com/v1/chat/completions",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${Deno.env.get("OPENAI_API_KEY")}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: "gpt-4",
        messages: [{ role: "user", content: message }],
      }),
    }
  );

  const data = await response.json();
  return new Response(JSON.stringify(data));
});

Step 3: Verify authentication is actually enforced

Time: 15 minutes | Impact: High

Lovable generates login pages, protected routes, and signup flows that look enforced. The catch: most of that enforcement lives in the React component. The data path underneath usually doesn't care whether you're logged in.

Test your protected pages

  1. Log out of your app.
  2. Paste a protected URL directly into a fresh incognito tab — /dashboard, /settings, /account.
  3. Each one should redirect to login. If the page flashes content for half a second before redirecting, your check is client-side only — the data was already fetched.

Test the data path, not just the UI

This is the step most builders skip. Open DevTools, go to the Network tab, and log in. Every request to your-project.supabase.co/rest/v1/... is your real API. Copy one as cURL and strip the Authorization header:

# Anon key only — what an unauthenticated attacker has
curl "https://YOUR-PROJECT.supabase.co/rest/v1/orders?select=*" \
  -H "apikey: YOUR_ANON_KEY"

If that returns data, the UI "protection" is theatre. RLS (Step 1) is your real authentication boundary.

The Edge Function gotcha almost everyone misses

When the Lovable AI creates a Supabase Edge Function for you — a checkout handler, a webhook, an AI proxy — the default supabase/config.toml entry looks like this:

[functions.ai-chat]
verify_jwt = false

verify_jwt = false means anyone on the internet can POST to that function. No login, no session, no token. If that function calls OpenAI, someone can drain your API budget in an afternoon. If it writes to your database, they can write whatever they want.

Flip it on for every function that shouldn't be public:

[functions.ai-chat]
verify_jwt = true

Then redeploy. Inside the function, read the user from the verified JWT instead of trusting anything in the request body:

const authHeader = req.headers.get("Authorization")!;
const supabase = createClient(
  Deno.env.get("SUPABASE_URL")!,
  Deno.env.get("SUPABASE_ANON_KEY")!,
  { global: { headers: { Authorization: authHeader } } },
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response("Unauthorized", { status: 401 });

Step 4: Test for IDOR

Time: 10 minutes | Impact: High

IDOR — Insecure Direct Object Reference — is the "change the ID in the URL and see someone else's data" bug. It's the single most common finding in our scans of Lovable apps, because the AI loves to pass raw database IDs through the client.

The test

  1. Create two accounts: A and B.
  2. Log in as A. Create something — an order, a note, an invoice. Watch the Network tab as you click around and note the IDs in the URLs and request payloads.
  3. Log in as B in a different browser profile.
  4. Replace B's IDs with A's. Hit the endpoint. Reload the page.

If B can read A's data, you have an IDOR.

Fix it — the Lovable way

Most Lovable apps call Supabase directly from the client using hooks the AI scaffolded. That means your "ownership check" has to live in your RLS policy, not in application code:

-- Only the owner can read, update, or delete their orders
CREATE POLICY "Owner-only access to orders"
ON orders FOR ALL
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

With that policy in place, supabase.from('orders').select('*').eq('id', someId) silently returns an empty array when someId belongs to another user. Try it in the Network tab — you should see a 200 with [], not the row.

If you wrote server-side routes or Edge Functions that use the service role key, RLS does not apply. You have to check ownership yourself:

// Inside an Edge Function using the service role client
const { data: order } = await supabase
  .from("orders")
  .select("*")
  .eq("id", orderId)
  .eq("user_id", user.id) // Must check yourself — RLS is bypassed
  .single();

This is the most common IDOR pattern in Lovable apps: the AI scaffolds an Edge Function with the service role key (so it can "do admin things"), and nobody adds the ownership check. The RLS policy you wrote in Step 1 does nothing here — service role bypasses it completely.


Lovable app security layers — auth, RLS, env vars, headers, monitoring around the deployed app
Lovable app security layers — auth, RLS, env vars, headers, monitoring around the deployed app

Step 5: Add security headers

Time: 5 minutes | Impact: Medium

Security headers tell browsers how to handle your page: don't embed it in an iframe, don't guess MIME types, only talk to you over HTTPS. Lovable's default preview domain (*.lovable.app) does not send any of them. If you've connected a custom domain through Vercel, Netlify, or Cloudflare Pages, you control them yourself. Pick your host below.

For Vercel deployment (vercel.json)

{
  "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=31536000; includeSubDomains"
        },
        {
          "key": "Permissions-Policy",
          "value": "camera=(), microphone=(), geolocation=()"
        }
      ]
    }
  ]
}

For Netlify (_headers file)

/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  Permissions-Policy: camera=(), microphone=(), geolocation=()

Step 6: Lock down file uploads

Time: 5 minutes (if applicable) | Impact: Medium

If your Lovable app lets users upload files (images, documents, etc.), check that:

  1. File types are validated. Only accept the types you expect (e.g., images only: JPEG, PNG, WebP).
  2. File sizes are limited. Set a reasonable max (5MB for images, 10MB for documents).
  3. Files go to Supabase Storage, not your app server. Supabase Storage handles files safely in a separate domain.
  4. Storage bucket policies are set. In your Supabase dashboard, check that storage buckets have appropriate access policies.

The same Lovable RLS pattern applies here. By default, Supabase Storage buckets are created with no policies — meaning anyone with your anon key can list or download every file. Run this check in the SQL Editor:

SELECT bucket_id, name, owner
FROM storage.objects
LIMIT 20;

If that returns rows without authentication, your storage is open. Lock it down with an ownership policy:

-- Only the file owner can access their uploads
CREATE POLICY "Users access own uploads"
ON storage.objects FOR ALL
USING (auth.uid()::text = (storage.foldername(name))[1]);

Step 7: Turn on monitoring

Time: 5 minutes | Impact: Medium

Even after the checklist, you want to know the moment something weird happens — before your OpenAI bill hits four figures overnight.

Supabase logs

In your Supabase dashboard:

  1. Logs & Analytics > API Logs — scan for requests with unusually large response bodies or 200 responses to tables that should require auth. A spike in /rest/v1/profiles?select=* is the telltale sign of someone pulling the whole table.
  2. Logs & Analytics > Auth Logs — watch for bursts of failed logins from the same IP, or a flood of signups right after you tweet your launch.
  3. Database > Roles — confirm only authenticated and anon have access to your public schema. If you see PUBLIC with broad grants, revoke them.

Key abuse

If you call paid APIs, set hard ceilings before you set "alerts":

  • OpenAI / Anthropic: set a monthly hard limit (not just soft alert) in your billing settings. $50 is plenty for a launch day.
  • Stripe: enable Radar rules for high-volume card testing and watch charge.failed webhooks.
  • Supabase Auth: turn on email confirmation so bot signups don't instantly burn your SIGNUPS_PER_HOUR quota.

The complete Lovable security checklist

Here's everything in one list. Work through it top to bottom:

  • RLS enabled on every Supabase table
  • RLS policies created for every table
  • No secrets in client-side JavaScript (except anon key and publishable keys)
  • All exposed secrets rotated
  • Protected pages redirect to login when unauthenticated
  • API endpoints return 401 without valid auth
  • Users cannot access other users' data (no IDOR)
  • Security headers configured on deployment
  • File uploads validated for type and size (if applicable)
  • Supabase Storage policies configured (if applicable)
  • Usage alerts set on third-party APIs
  • Supabase auth logs reviewed

What comes after the manual review

Work through this checklist and your app is already in better shape than most Lovable apps running in production today.

The catch: every new feature is a new chance to regress. The AI adds a table next week — no RLS. It scaffolds a new Edge Function — verify_jwt = false. You wire up a new service role call — no ownership check. The checklist is a one-time audit. Your app isn't a one-time project.

Manual reviews drift. Automated scanning doesn't.

Flowpatrol runs every check on this page — plus dozens more — against your live URL. Paste the URL, get a report in five minutes, fix what matters. Run it after every feature push, or wire it into CI so it catches regressions before they ship.

Scan your Lovable app free


This guide is current as of March 2026. The Supabase steps apply to any app using Supabase, whether it was built by Lovable, Bolt, Cursor, or hand-written.

Back to all posts

More in Guides

npm Supply Chain Hygiene for Vibe Coders
Apr 4, 2026

npm Supply Chain Hygiene for Vibe Coders

Read more
AI Agent Safety: What Your Agent Can Destroy (And How to Stop It)
Apr 3, 2026

AI Agent Safety: What Your Agent Can Destroy (And How to Stop It)

Read more
How to Secure Your MCP Setup
Apr 3, 2026

How to Secure Your MCP Setup

Read more