Web Security

You built it with Lovable or Cursor: 10 things to check before real users arrive

Back to Articles
2026-11-01
12 min read
MAAuthor Marius Andronie
You built it with Lovable or Cursor: 10 things to check before real users arrive

In short: an app built with Lovable, Bolt, Cursor or another AI coding agent can work perfectly in the demo and still leave keys, data or admin screens open to anyone who looks. Below are ten checks you can run yourself before real users arrive. Each one says what to look for and how to check it, with a link to the documentation behind it.

These checks catch common, visible mistakes. They do not prove that an app is secure, and they are not a security test. If a check turns up something you cannot fix or cannot explain, that is the moment to get help.

You need two things for most of them: the live app open in a desktop browser with the developer tools (press F12), and two test accounts that belong to two different made-up users.

1. API keys and tokens in client code

What to look for. Anything that runs in the browser can be read by whoever opens your site. According to Lovable's FAQ, apps created since May 2026 use TanStack Start and older ones use React and Vite. Both can use Vite's rule: every variable whose name starts with VITE_ is written into the JavaScript the browser downloads. The Vite documentation says these variables should not contain sensitive information such as API keys. Next.js does the same with the NEXT_PUBLIC_ prefix: the Next.js documentation says such a value is inlined into the JavaScript sent to the browser.

Some keys are meant to be public. The Supabase anon or publishable key is designed for the browser, and a Stripe key that starts with pk_ is a publishable key. Others must never be there: the Supabase service_role or secret key, which bypasses row level security, a Stripe secret key that starts with sk_live_ (Stripe's key types), and keys for AI model providers, email or SMS services.

How to check it yourself.

  • Open your live app, then the developer tools. In the Sources tab, search all files for service_role, sb_secret, sk_live, secret and apiKey.
  • In the Network tab, reload the page and look at requests going straight from the browser to a paid service, such as an AI model or an email provider. A request like that carries its key with it.
  • In your code, search the same words, and look at every variable with a VITE_ or NEXT_PUBLIC_ prefix. Ask of each one: would I be fine with a stranger reading this?

If you find one. Treat the key as exposed: create a new key at the provider and delete the old one. Then move the call that needs it into a server function, so the browser asks your server and only your server holds the key. Deleting the key from the code is not enough if the code is in git, as we explain in a deleted key is still in your git history.

2. Supabase without row level security

What to look for. If your app uses Supabase, the browser reads and writes the database directly through Supabase's API, using the public key from check 1. Row level security (RLS) is the set of rules that decides which rows each user may read or change. The Supabase documentation says RLS must be enabled on every table in an exposed schema, and the public schema is exposed by default. Supabase's own database checker describes the risk plainly: on a public table without RLS, anyone with the project's URL can create, read, update and delete rows.

RLS switched on is not the end of it. A policy written as using (true) lets every caller read every row, so the table is open again.

How to check it yourself.

  • In the Supabase dashboard, open the Security Advisor and read every warning about RLS.
  • In the SQL editor, list your tables and whether RLS is on. The rowsecurity column comes from PostgreSQL's pg_tables view:
select tablename, rowsecurity
from pg_tables
where schemaname = 'public';
  • Any row with false needs RLS turned on and policies written. For each table that has policies, read them. A policy for user data usually compares the row's owner with auth.uid(), the ID of the signed-in user.
  • Then test it the way a stranger would: log in as your first test user, create some data, log in as the second test user and try to see it.

3. Open admin pages

What to look for. An admin page is not protected if the only protection is a hidden menu link, or a role check that runs in the browser before the page is shown. Neither stops someone who types the address, and neither stops someone who asks for the data behind the page directly. OWASP lists this under broken access control, with exactly these examples: a logged-out user reaching a page that needs a login, and a normal user reaching an admin page.

How to check it yourself.

  • Open a private window and type the addresses of your admin and internal pages directly, such as /admin, /dashboard or /settings. You should be sent to a login page, and no data should load.
  • Log in as a normal test user and try the same addresses.
  • Log in as an admin, open the Network tab and note which requests the admin page makes. Then repeat those requests while logged in as a normal user. The server, or your RLS policies, must refuse them. A page that shows nothing while the data still arrives in the Network tab is not protected.

4. Missing security headers

What to look for. Security headers are short instructions your server sends with every page. They tell the browser to use https only, not to guess file types, not to let other sites frame your pages, and which scripts may run. The OWASP HTTP headers cheat sheet describes each one. The usual set is:

  • Strict-Transport-Security, which keeps the browser on https.
  • X-Content-Type-Options: nosniff, which stops the browser guessing file types.
  • Content-Security-Policy, which limits where scripts and other content may load from. Its frame-ancestors part, or the older X-Frame-Options header, controls who may put your page in a frame.
  • Referrer-Policy, which limits what your pages tell other sites about where a visitor came from.

How to check it yourself. Run this in a terminal, with your own address:

curl -sI https://your-app.com

and look for those names in the reply. Or use our free website security headers check, which reads the headers of your home page and explains each result in plain words. Headers are usually set in your hosting configuration, not in the app code. Add the Content-Security-Policy last and test the app after it, because a strict policy can block scripts your app needs.

5. Error pages that leak details

What to look for. When something fails in production, the visitor should see a short, plain message, and the details should go to a log only you can read. The OWASP error handling cheat sheet recommends exactly that. A stack trace, a file path on the server, a database query or a library version in an error message tells an outsider how the app is built. MITRE lists this weakness as CWE-209.

How to check it yourself.

  • Open an address that does not exist, such as /this-page-does-not-exist.
  • Submit forms with wrong input: an empty required field, letters in a number field, a very long text.
  • With the Network tab open, read the replies to requests that fail, not only what the page shows. An API often returns the full error even when the page shows a friendly message.
  • In the Sources tab, check whether your original source files appear in readable form. If they do, the build is publishing source maps. Vite's build.sourcemap option is off by default, so if they are on, decide on purpose whether you want your source readable by anyone.

6. Change the ID in the address

What to look for. Many apps show a record at an address like /orders/1042 or /invoices/a8f3.... If the server returns whatever ID it is asked for, any user can read other users' records by changing the number. OWASP puts this first in its API risks as broken object level authorization.

How to check it yourself.

  • Log in as your first test user and open one of their records. Note the ID in the address bar or in the Network tab.
  • Log in as the second test user, paste the same address, and replay the same request.
  • You should get "not found" or "not allowed". If you see the first user's data, the check that the record belongs to the user who asks for it is missing, on the server or in the RLS policy.

7. Files anyone can open

What to look for. Uploaded files are easy to forget. In Supabase Storage, a bucket marked public can be read by anyone who has the file's address, with no login. That is fine for product images and avatars. It is not fine for invoices, contracts, ID documents or anything a user uploads expecting privacy.

How to check it yourself.

  • In the Supabase dashboard, open Storage and note which buckets are public and what kind of files each one holds.
  • Upload a private file as a test user, copy its address, and open it in a private window. If it opens, anyone with the link can open it.
  • Private files belong in a private bucket, shown to the right user through short-lived signed links, with storage policies that say who may read what.

8. Paid services anyone can trigger

What to look for. AI features, SMS, emails and file processing cost money per use. If an endpoint that calls a paid service can be reached without logging in, or with no limit per user, someone can run it thousands of times. The bill is yours, and an open email or SMS endpoint can be used to send messages in your name. OWASP calls this unrestricted resource consumption.

How to check it yourself.

  • List every action in your app that costs money: each AI call, each SMS, each email, each upload that gets processed.
  • For each one, check whether it works when logged out, and whether anything stops one user from triggering it a hundred times in a minute.
  • At each paid provider, open the billing settings and set a usage limit or a spending alert if the provider offers one.
  • Check that sign-up and contact forms have some protection against bots, so they cannot be used to send emails in your name.

9. Dependencies with known vulnerabilities

What to look for. Your app runs on packages written by other people, and each package brings in more. Known vulnerabilities in public packages are published, and npm can compare your project against them.

How to check it yourself. In the project folder, run:

npm audit

The npm documentation explains the report: each package with a known vulnerability, how serious it is, and whether a fixed version exists. Then:

  • If the code is on GitHub, turn on Dependabot alerts, so you hear about new vulnerabilities without remembering to look.
  • Update one thing at a time and test the app after each update. Running npm audit fix --force can install new major versions that change how a package behaves.

10. Backups, and a restore you have actually tried

What to look for. Real users bring real data, and a single wrong command, from you or from an AI coding agent, can delete it. A backup only counts once you have restored from it. Supabase's backups documentation explains what each plan includes, including how many days of backups are kept and whether point-in-time recovery is available. The same page notes that database backups do not include the files stored in Supabase Storage.

How to check it yourself.

  • In the Supabase dashboard, open the backups page and confirm what your plan actually keeps, and for how long.
  • Make your own export with the Supabase CLI and keep it somewhere outside Supabase:
supabase db dump -f backup.sql
  • Restore that export into a separate, empty test project, and open the app against it.
  • Decide how you back up the files in Storage, since the database backup does not hold them.
  • Write down the steps, so a restore at a bad moment is a list to follow, not a search.

What these checks do not cover

Ten checks are a starting point. They do not look at payment flows, login and password reset in depth, how the app behaves under load, privacy obligations for the data you collect, or whether the code can be maintained by someone other than the AI agent that wrote it. A clean result on all ten means these ten problems were not found, nothing more.

When to get help

If a check turns up something you cannot fix, or the same problem comes back every time you ask the AI coding agent to fix it, a second pair of eyes is usually faster than another prompt. Our AI app rescue is built for apps made with Lovable, Bolt, Cursor and similar tools. It starts with a fixed-price diagnosis: a written report of what is wrong, why, and a fixed price for each fix. The report is yours to use with any developer.

Frequently asked questions

Is it safe to have the Supabase anon key in my frontend code? Yes, that key is designed to be public, but only while row level security is enabled on every exposed table and the policies are correct. The service_role or secret key bypasses row level security and must never be in browser code.

Can I ask the AI coding agent to run these checks for me? It can help with parts of them, such as searching the code for keys or drafting RLS policies. Verify the result yourself on the live app with two test accounts, because the agent sees the code, not your production settings or what a stranger's browser receives.

Do these ten checks mean my app is secure? No. They catch common mistakes that you can see from a browser, the dashboard and the code. They are not a security test, and passing them says nothing about problems they do not look at.

What should I do first if I find a secret key in my client code? Create a new key at the provider and delete the old one, then move the call that needs the key to a server function. Removing the key from the code alone is not enough, because it stays in git history and in any copy of the site already downloaded.

Which security headers matter most before launch? Strict-Transport-Security, X-Content-Type-Options and a framing rule are low-risk to add. Content-Security-Policy gives the most protection but can break scripts, so add it last and test the app after each change.

Do I need a developer to fix what I find? Not always. Turning on RLS, making a bucket private or setting a spending limit can be done in a dashboard. Moving a key to a server function, rewriting access rules or fixing error handling usually needs someone who can read and test the code.

Read the whole book: Agents You Can Leave Alone

Harness engineering for AI systems in production: the guards, checks and habits that let an AI agent run without someone watching it, each one taken from a real failure on our own systems. The articles on this blog come from its chapters.

Need a system like this built for your company? Custom AI and Python builds are scoped and priced in writing, with no sales call.

Opens in a new tab