The Lovable Production Checklist.
14 things to verify before real users hit your Lovable app. The same checklist I run for every paid audit. Free, public, copyable.
1. Secrets and API keys
The most common findings in Lovable audits are exposed keys. Fix this section first. These take minutes to check and minutes to fix, and they prevent the worst outcomes.
Supabase service role key is NOT in client code
WhyThe service role key bypasses Row Level Security and has full admin access to your database. If it ships in your client bundle, anyone with browser DevTools can read or delete every row in every table, including auth.users.
CheckOpen your live site, hit DevTools (F12), go to Sources tab, and search for service_role or SUPABASE_SERVICE_ROLE_KEY. If anything shows up, you have a problem.
FixMove the key to a server-side environment variable only. Move any client-side calls that needed it into API routes (Vercel functions, Next.js API routes, etc.) and call those routes from the client instead.
No hardcoded API keys for OpenAI, Stripe, Anthropic, etc.
WhyHardcoded keys in your repo (especially if it's public, but even private repos get compromised) can rack up thousands in API charges before you notice. AI provider keys especially. Stripe secret keys are worse.
CheckIn your codebase, search for sk_live, sk_test, sk-, Bearer, and any key prefixes for services you use. Anything that looks like an actual key should be a reference to an env var.
FixMove every key to environment variables. Rotate any key you find in code — it's already compromised. Add a .env entry to .gitignore.
2. Database access
Lovable scaffolds Supabase tables but rarely scaffolds the policies that protect them. This is the silent disaster: your app feels secure because users have to log in, but any logged-in user can read or write any row.
Row Level Security (RLS) is enabled on every table
WhyIf RLS is off, anyone who knows your anon key (which IS in your client bundle and that's fine) can read every row in that table. The anon key is supposed to be safe because RLS controls what it can do. Without RLS, it's a master key.
CheckIn Supabase dashboard → Authentication → Policies. Look at every table you created. If any of them say "RLS disabled" or "0 policies," that table is wide open.
FixEnable RLS on every table (Supabase will warn you). Then write policies for that table before you turn it back on for real users.
RLS policies actually restrict access — not just "authenticated can do anything"
WhyA common Lovable pattern is a single RLS policy that says "USING (auth.uid() IS NOT NULL)" which means "any logged-in user can read this row." That's not real access control. Every user can read every other user's data.
CheckFor each table, ask: should every authenticated user see every row here? If the answer is no (which it usually is for tables like profiles, orders, messages), your policy needs to filter by user_id = auth.uid().
FixRewrite the policy to use a row-level filter. Example: USING (user_id = auth.uid()) for "users can only see their own rows."
3. Auth and access control
RLS protects your database. But API routes that call the database with the service role key bypass RLS entirely. That means YOUR code has to enforce who can do what.
Every API route checks auth before returning data
WhyLovable scaffolds API routes for things like "fetch user profile," "save settings," "list orders." If those routes don't verify who is calling them, anyone can call them by hitting the URL directly. Auth checks in your React components don't protect API routes.
CheckOpen your terminal and run curl https://yourapp.com/api/[any-route] without logging in. If you get data back, that route doesn't check auth.
FixAt the top of every API route, get the session from the request. If there's no session, return 401. If the route accesses user-specific data, also verify the user owns that data.
No IDOR — changing one number in a URL doesn't reveal someone else's data
WhyInsecure Direct Object Reference (IDOR) is the second most common finding in Lovable apps. The pattern: a route like /api/orders/[id] looks up the order by ID without checking if the requesting user owns that order. Anyone can iterate through IDs and see everyone's data.
CheckLog in as user A, find a URL that includes an ID (like /orders/123 or /api/users/456). Change the number. Do you see someone else's data?
FixIn every route that takes an ID, add a check: does the current user own this row? If not, return 403. For SELECT queries via Supabase, RLS catches this automatically — but only if you're not using the service role key in that route.
Rate limit on /signup and /login
WhyWithout rate limiting, attackers can run credential-stuffing attacks (trying leaked username/password combos at scale) or signup floods (creating thousands of fake accounts) against your endpoints. Even moderate abuse can rack up your Supabase or email costs fast.
CheckLook in your auth routes (or middleware) for any rate-limiting logic. If there's none, you're unprotected.
FixAdd rate limiting at the edge (Vercel's middleware, Cloudflare, or a service like Upstash Rate Limit). A simple rule: 5 attempts per IP per minute on /login, 3 signups per IP per hour.
4. Payments
Only applies if you've added Stripe (or similar) to your Lovable app. If you have, these two checks are non-negotiable. Skipping either can mean fake payments or wrong prices.
Stripe webhook signature is verified
WhyStripe sends payment confirmations to a webhook URL you set up. If you don't verify the signature on incoming webhooks, anyone can send fake payment-success events to your endpoint and trigger your "mark as paid" logic. Free product, your bill.
CheckOpen your webhook handler. Look for stripe.webhooks.constructEvent or similar signature verification. If you're just parsing the JSON body and trusting it, you have a problem.
FixUse Stripe's constructEvent(rawBody, signature, webhookSecret). Set STRIPE_WEBHOOK_SECRET in your env. Reject any event that fails verification.
The server determines prices, not the client
WhyIf your client code sends a price to Stripe (or to your server) when creating a checkout session, anyone can intercept and change the price. Your $99 product gets sold for $1.
CheckIn the code that creates a Stripe checkout session, ask: where does the price come from? If it's coming from the client (URL param, form field), that's bad. If it's looked up server-side from a database or hardcoded, that's good.
FixLook up prices server-side based on a product ID. The client can send "I want product A" — the server decides what product A costs.
5. Validation and errors
Client-side validation is for UX. Server-side validation is for safety. Lovable usually gives you the first and forgets the second.
Server-side input validation on every endpoint
WhyThe browser only validates what your UI shows. An attacker hits your API directly with curl or a script, with whatever payload they want. Without server validation, they can submit 10MB strings, SQL injection attempts, scripts that get rendered as HTML somewhere, or just data that breaks your business logic.
CheckFor every API route, ask: am I validating that the input is what I expect (string of expected length, valid email, etc.)? If you're just destructuring the body and using it, that's not validation.
FixUse a schema validator like Zod, Yup, or Valibot at the top of every route. Reject anything that doesn't match the schema. Return a clean 400 error.
Error boundaries in React so one broken component doesn't white-screen the whole app
WhyIf a React component throws an uncaught error, by default the entire app unmounts and the user sees a white screen. Lovable rarely scaffolds error boundaries. One bad data row, one missing field, one API timeout can take the whole app down for that user.
CheckOpen your code. Search for ErrorBoundary or componentDidCatch. If you find nothing, you have no protection.
FixWrap your top-level layout in an ErrorBoundary component. Wrap any data-fetching or risky component (uses third-party state, parses user input, etc.) in its own boundary. Render a "something went wrong, try again" UI instead of unmounting.
6. Deploy and logs
The last category is the easiest to miss because it's invisible. Nothing looks broken when you ship. The problems show up at 2am, weeks later, when you're trying to figure out why something went wrong.
CORS is not set to * in production
WhyIf your API allows requests from any origin (Access-Control-Allow-Origin: *), any malicious site can make authenticated requests to your API on behalf of your logged-in users. This breaks the whole same-origin protection model browsers give you.
CheckLook at your CORS configuration (middleware, framework config, or response headers). If it says * or includes wildcards, that's a problem.
FixSet CORS to only allow your own domain(s). Example: Access-Control-Allow-Origin: https://yourapp.com. If you need multiple, maintain an allowlist.
No sensitive data in console.log calls
Whyconsole.log calls run in the user's browser. Whatever you log there, the user can see. If you're logging tokens, passwords, full user objects, Stripe customer IDs, or internal IDs that shouldn't leak, you're handing them to anyone who opens DevTools.
CheckSearch your codebase for console.log, console.error, console.warn. Look at what's being logged. If any of it is sensitive, that's a leak.
FixStrip console calls before production (most build tools support this). For genuine debugging, log only IDs you've already exposed (like the URL path) or use a real server-side logger.
Environment variables are separated between dev and prod
WhyIf you reuse the same Stripe key, Supabase project, and database between development and production, every test you run on your laptop is a real transaction or a real change in your real database. One day you'll wipe your production data thinking it's the dev copy.
CheckIn your Vercel (or other host) project settings → environment variables. Are dev and production showing different keys, or the same? Same = problem.
FixCreate a second Supabase project for dev. Get a Stripe test mode key (starts with sk_test_). Configure your host so dev/preview deployments use the dev keys, and production uses the prod keys.
Found something on this list in your app?
Two paths from here.
Fix it yourself. Each item above has specific instructions. Most take under an hour. The list is yours to keep, share, and re-run any time you ship something new.
Have someone do it for you. The Rivetz audit walks every item, finds the issues specific to your app, and gives you a written report with prioritized fixes. $1,000 flat. 3 business days. No scheduled calls.
Book the audit →