← Back to blog
Jun 28, 2026·14 min read

Is Your Vibe-Coded App Actually Secure?

Is Your Vibe-Coded App Actually Secure?

You described an app, an AI built it, and it works. You can sign up, log in, save data, and the UI looks like a real product. Real users could sign up tomorrow. So here is the uncomfortable question almost nobody asks until it's too late: is it actually safe?

This is the part of vibe coding that gets skipped, partly because security sounds like something only real engineers worry about, and partly because the app looks fine. It loads, it logs you in, it saves your data. But looking fine and being safe are different things, and the gap between them is exactly where vibe-coded apps get embarrassed - leaked user data, exposed payment keys, a database anyone with a browser can read, an admin panel a stranger can walk into.

The good news: you don't need to become a security expert. You need to understand a small number of things that actually break, and how to check each one in plain language. That's what this guide is. No fear-mongering, no jargon - the five real risks for an AI-built app in 2026, the exact pre-launch tests that catch them, and what to do if you find a problem. You can run this entire audit in an afternoon.

Why this matters even for a small app

A common and dangerous assumption is that security is a big-company problem, that no one would bother attacking your little tool. That's backwards. Small apps get hit precisely because they're easy. Automated bots scan the entire public internet constantly, looking for exposed databases, leaked API keys, open admin endpoints, and default credentials. They don't care how big you are. They care whether the door is open. A new domain with an exposed Postgres instance is typically found within hours of being deployed.

The stakes are personal. If your app holds even basic user data - names, emails, anything people typed in trusting you - a leak isn't just a bug. It's your users' information in the wrong hands, with your name on it. And it has real consequences: legal exposure under GDPR and CCPA, hosting costs if your platform gets used to scrape data, lost trust you don't get back, and in the worst cases, the kind of viral incident that ends a product before it starts.

The reassuring part: the failures are predictable. The same five things break on vibe-coded apps over and over, which means you can check for all of them in an afternoon. The rest of this article is exactly that audit.

The five things that actually break

In rough order of how often they cause real incidents on vibe-coded apps:

1. Your data is readable by anyone

This is the most common failure, by a wide margin, and the most damaging. The risk is simple to state: your app has a database, and the question is whether each user can only see their own rows, or whether anyone - logged in or not - can read everyone's.

When an AI builds your app, it will happily make the app work - sign up, log in, save data - without necessarily locking down who can read what. The result is an app that looks correct because when you're logged in as you, you see your data. But the rules underneath might allow any logged-in user, or in the worst case any anonymous visitor, to query the entire table.

The mechanism behind this on Postgres-based backends (Supabase, Butterbase, and others) is called Row Level Security, or RLS. RLS lives in the database itself, not in your app code, which is what makes it trustworthy: a malicious user can manipulate API calls all day long, but they can't bypass a rule the database is enforcing on every read and write. Without RLS, your data is only as safe as the most careless line of frontend code that touches it - and frontend code is meant to be inspected by users.

How to check (the two-account test):

  1. Create two separate accounts - call them A and B. Use two different browsers or one normal window and one incognito window so the sessions don't collide.
  2. Log in as A. Create some data - a project, a note, a record, whatever your app has.
  3. Log in as B in the second window. Try to see A's data through the normal UI. You shouldn't.
  4. Now the harder test: in B's browser, open developer tools, find one of your app's data-fetching network calls, and replay it with A's record ID. You should get back nothing, or a 403 Forbidden - never A's data.
  5. Bonus: log out entirely and try the same replay. The endpoint should reject anonymous reads of user data.

If any of those steps return A's data to B (or to nobody), you have the most common and most damaging vibe-coding vulnerability. Tell your AI tool explicitly: "Enable row-level security on every table that holds user data, and add policies so each user can only select, insert, update, and delete their own rows." Then re-run the test until B is properly walled off.

2. Secret keys are exposed in the app

Apps use secret keys for payments (Stripe), for sending email (Resend, Postmark, SendGrid), for talking to AI models (OpenAI, Anthropic), for cloud services. These are like passwords with a credit card attached. They belong on the server side - never in the part of the app that runs in the user's browser.

The failure here is that a secret key ends up in the frontend, where anyone can open browser developer tools, view the page source, or read a bundled JavaScript file and find it. With your Stripe secret in hand, a stranger can refund themselves; with your email key, they can send mail in your name; with your AI provider key, they can run up thousands of dollars of usage in a weekend. Every one of these has happened to vibe-coded apps in the last year.

The distinction that catches people: most services give you two keys - a public/publishable key that's safe to ship to the browser, and a secret/service key that must never leave the server. The names are explicit. If a key is labeled publishable or public, it's fine in frontend code. If it's labeled secret, private, service_role, or server, it never goes there.

How to check:

  1. Open your deployed app in a browser. Right-click and pick "View page source," then search the page for "sk_", "secret", "service_role", and "private". You should find none of them.
  2. Open developer tools, go to the Network tab, reload the page, and inspect the bundled JavaScript files. Search the same terms there. Again, none.
  3. Ask your AI agent directly: "List every secret or API key in this project and tell me whether each one is used only on the server, or also shipped to the browser. Move any that are exposed to server-side functions."
  4. If you've ever accidentally committed a secret to a public Git repo, rotate the key immediately, even if you've since removed it. Bots scrape Git history continuously.

The same rule applies to environment variables: anything prefixed with VITE_, NEXT_PUBLIC_, REACT_APP_, or similar gets bundled into the frontend. Secrets must never use those prefixes.

3. Anyone can pretend to be logged in, or skip login entirely

Authentication is who you are. Authorization is what you're allowed to do. Vibe-coded apps often get the first roughly right and the second badly wrong - the login screen works, but the actual protected pages or API endpoints aren't truly checked on the server.

The failure pattern is this: the frontend hides a page or a button unless a logged-in flag is true. That feels like protection, but it's only protection from honest users. The data behind the hidden UI is still reachable directly by visiting the URL, calling the API, or replaying a network request. Hiding a button is not the same as protecting what it does.

A second, subtler version: the app correctly checks that you're logged in, but not which user you are, when it processes a request. A request to "delete project 42" succeeds for anyone who's logged in, regardless of whether project 42 belongs to them. This is called a Broken Object Level Authorization (BOLA) bug, and it's in the OWASP top 10 for a reason - it's everywhere, and easy to miss.

How to check:

  1. Log out completely. Try to visit every "protected" page by typing the URL directly. You should be redirected to login, not see the page briefly before redirecting.
  2. Logged out, try to call your app's data API endpoints directly with curl or Postman. They should return 401 Unauthorized.
  3. Re-run the two-account test from #1, but this time try destructive actions: from account B, attempt to update or delete account A's records by editing the ID in the request. The server should refuse, every time.
  4. Check that admin or moderator capabilities are gated by a real server-side role check, not by a feature flag in the frontend. Never store user roles on the user's own profile row - they belong in a separate table with their own access rules, or a privilege-escalation bug is almost certain.

4. The app trusts whatever a user types

Users - and bots pretending to be users - will put unexpected things into your forms, on purpose. Long strings, SQL fragments, JavaScript snippets, file uploads with executable content, deliberately malformed JSON. If your app blindly trusts that input, it can be tricked into exposing data, running attacker-controlled code, or behaving in ways you didn't intend.

You don't need to memorize the technical names (SQL injection, XSS, CSRF, SSRF) to defend against them. You need to know the principle: input is untrusted until it's validated, and output is unsafe until it's escaped. Modern frameworks and parameterized database queries handle most of this for you if you use them correctly - which is the thing AI tools sometimes skip.

How to check:

  1. In any text input, try pasting a long HTML snippet like <img src=x onerror=alert(1)>. Save it. View the result somewhere the value gets displayed. If you see an alert popup, the app is rendering raw HTML - fix it before launching.
  2. In a search or filter field, paste a SQL fragment like ' OR 1=1 --. The app should treat it as a literal search string, not crash and not return everything.
  3. Ask your AI agent: "Confirm that every database query uses parameterized queries or an ORM, never string concatenation. Confirm that user-supplied HTML is escaped on render, never injected via dangerouslySetInnerHTML or innerHTML."
  4. For file uploads, confirm the server validates file type and size, and stores files somewhere they cannot be executed as code (a CDN or object store, not a public web server directory).
  5. Confirm that any AI prompt that includes user input treats the user input as untrusted - prompt injection is the 2026 equivalent of SQL injection, and it can leak system prompts or chained credentials.

5. There is no plan for when something goes wrong

This one is less an attack and more a quiet liability. If your app has no backups, no audit log, and no monitoring, then the day a problem happens - a mistake, a bad actor, a bug, or just bad luck - you have no way to recover and no way to even know it happened. You'll find out from an angry user, which is the worst possible way.

The four things to confirm before launch:

  1. Backups exist and you've tested a restore. Most managed backends do daily snapshots automatically. Confirm yours does, find where they live, and at least once try to spin up a copy from a backup so you know the process works. An untested backup is a hope, not a plan.
  2. You'd notice if the app went down. A free uptime monitor (UptimeRobot, BetterStack, Pingdom) that emails you if the app stops responding is fifteen minutes of setup and saves you from hearing about outages from customers.
  3. You'd notice if something started going wrong. Error logs from your backend and a basic error tracker (Sentry has a generous free tier) catch bugs that don't take the whole site down - slow leaks like a broken signup flow or a payment that's silently failing for 5% of users.
  4. You have a way to delete a user's data. GDPR and the Apple App Store both require it. Build the deletion flow on day one - retrofitting it later is painful because you have to find every table that references a user ID.

The pre-launch security checklist

Before real users touch your app, work through this list with two browsers open - one normal, one incognito. None of it requires coding knowledge, just a willingness to actually test.

  • Two-account test passes: account B cannot read or modify account A's data, through the UI or by replaying API calls.
  • Logged-out test passes: protected pages redirect to login, protected APIs return 401 Unauthorized.
  • No secret keys, service keys, or private API keys appear in the page source, the bundled JavaScript, or the network tab.
  • No environment variable starting with VITE_, NEXT_PUBLIC_, or similar contains a secret.
  • Pasting <script>alert(1)</script> into a text input and viewing the result does not produce an alert.
  • Database queries use parameterized queries or an ORM - confirmed by asking your AI agent and spot-checking the code.
  • User roles, if any, live in a separate table - not on the user profile - and are checked with a server-side function.
  • Backups are automatic, and you've at least once seen them work.
  • Uptime monitor is configured. Error tracker is configured.
  • Account deletion flow exists and was tested end to end.
  • You're collecting only the data you actually need - the safest data to leak is the data you never collected.

If every box is checked, you've cleared the failures that sink the large majority of vibe-coded apps before they reach a hundred users.

What to do if you find a problem after launch

Most vibe coders panic and either freeze or overreact. Both are wrong. The right response, in order:

  1. Stop the bleeding. If a key is leaked, rotate it now - before fixing anything else. If a destructive endpoint is wide open, disable that endpoint or take the app offline temporarily. Containment first, diagnosis second.
  2. Estimate the scope. Was it exposed for an hour or a month? Was any data accessed (check logs)? How many users could be affected?
  3. Fix the underlying cause, not just the symptom. Don't just patch the one endpoint - re-run the relevant checklist item across the whole app.
  4. Notify affected users honestly if any user data was exposed. Vague non-disclosure is worse than a clear, calm email. In some jurisdictions (GDPR), notification is legally required within 72 hours.
  5. For serious incidents - confirmed access to sensitive personal data, financial loss, or regulated information - get a security professional involved. This is not the moment for DIY.

How your backend changes the difficulty

Here's the honest truth that ties all five failures together: most of these risks come down to the backend - the data, the auth, the access rules, the input handling, the backups. Which means the backend you build on largely determines how hard security is.

Two paths:

  • A backend that leaves all of this to you to configure by hand. You're responsible for getting every rule right. A single missed setting - RLS not enabled on one table, a service key in the wrong env var, a missing server-side check - is the leak. The default for new tables is wide open.
  • A backend that handles the dangerous defaults well. Real authentication out of the box, access rules you can set declaratively and verify quickly, parameterized queries by default, automatic backups, an audit log. The default for new tables is locked down.

For a non-engineer, the second path is dramatically safer - not because the first is bad, but because you don't have to remember to do the right thing on every single table, every single time. This is the strongest argument for building on established, modern infrastructure rather than improvising the backend.

Butterbase is built with this in mind: a real backend with proper authentication and access control, where your AI coding agent sets up the data rules directly, RLS is enabled by default on user tables, and the dangerous defaults are handled for you. We mention it not to pitch, but because the single most effective security decision a vibe coder can make is choosing infrastructure that does not make it easy to leave the door open. Whatever you build on, that's the standard to hold it to.

The takeaway

Your vibe-coded app can absolutely be secure. The reason so many aren't is that the building got easy while the checking got skipped, and the failures are invisible until someone finds them - usually a bot, sometimes a curious user, occasionally an attacker. But the failures are predictable, and now you know the five that matter: readable data, exposed keys, weak authorization, untrusted input, and no recovery plan.

Run the two-account test. Search the page source for secret keys. Replay an API call while logged out. Confirm your backups. Add a basic uptime monitor. Do it in an afternoon, before you tell anyone the app is live. You will be ahead of most apps that ship.

Security is not the scary part of vibe coding. Skipping it is.

This article covers common defensive security practices for protecting your own application. If you're dealing with an active security incident, regulated data, or sensitive user information at scale, consult a security professional rather than relying on a checklist.

Frequently asked questions

Run three tests. First, the two-account test: sign up twice, try to read and modify the first account's data from the second, both through the UI and by replaying network requests. Second, search your deployed page source and bundled JavaScript for the strings 'sk_', 'secret', and 'service_role' - none should appear. Third, log out and try to hit your data APIs directly - they should return 401 Unauthorized. Those three checks catch the failures behind the large majority of vibe-coded app leaks.

Because they're not picking you specifically. Automated bots scan the entire public internet constantly for open databases, leaked API keys, default credentials, and known vulnerabilities, then exploit anything they find at scale. They don't care how big you are - only whether the door is open. A new domain with an exposed Postgres instance is typically found within hours of being deployed.

Row Level Security (RLS) is a Postgres feature that enforces access rules inside the database itself, on every read and write. The reason it matters for vibe-coded apps is that a malicious user can bypass any check your frontend does, but they cannot bypass a rule the database is enforcing. Without RLS, your data is only as safe as the most careless line of frontend code that touches it - and frontend code is meant to be inspected by users. Enabling RLS with per-user policies is the single highest-impact security action you can take.

The two-account test. Sign up two separate accounts, create data with the first, then log in as the second and try to see or modify the first account's data - both through the UI and by replaying API calls with edited IDs. If you can, every user can see or destroy every other user's information. Fix that before anything else, by enabling Row Level Security on every user-data table and adding per-user policies.

Yes, meaningfully. Backends that enable Row Level Security by default on new tables, use parameterized queries automatically, take regular backups, and provide a clear separation between public and secret keys make it much harder to ship an open door by accident. Backends that leave all of that for you to configure by hand are only as safe as the configuration you remember to write - and a single missed setting is the leak.

Stop the bleeding first: rotate any exposed keys immediately, disable any wide-open endpoints, take destructive actions offline if needed. Then estimate scope from your logs, fix the underlying cause across the whole app (not just the one place you noticed), and notify affected users honestly. Under GDPR you may be legally required to notify within 72 hours. For confirmed access to sensitive personal data or financial information, get a security professional involved.

On the server, yes - that's exactly what environment variables are for. In the frontend, no. Any environment variable prefixed with VITE_, NEXT_PUBLIC_, REACT_APP_, or similar gets bundled into the JavaScript shipped to browsers, where anyone can read it. Secrets must use server-only variable names and be referenced only by server-side functions, edge functions, or backend code - never by code that runs in the user's browser.

Run the full pre-launch checklist again every time you add a new table, a new endpoint, or a new third-party integration - these are the moments new holes appear. A quick re-run of the two-account test and a search for exposed secrets takes fifteen minutes and catches the regressions that most often appear when an app is being actively developed. Set a quarterly calendar reminder for a deeper pass, including a backup restore test.