How to secure a vibe-coded app: a checklist
Reading progress: 14 min remaining
Eleven checks for an AI-built app, from leaked keys and Supabase RLS to CORS and rate limits, and which ones Vibely’s free security scan covers.
Short answer
To secure a vibe-coded app, check eleven things before launch: leaked API keys, secrets in the client bundle, Supabase row-level security, auth settings, input handling, vulnerable dependencies, CORS, rate limits, storage buckets, logs and publishing settings. Vibely’s free security scan checks the code, dependency and database items for you. CORS and rate limits need a manual pass.
Vibe coding moves fast because you describe what you want and an agent writes the code. Security doesn't move with it. The agent tests the happy path, you test it as the only user, and the gaps that matter only show up when a stranger with a browser console shows up. This checklist covers the eleven places a generated app most often leaks. Each item says why it matters, how to check it, how to fix it, and exactly what Vibely's security scan does about it, including the items it doesn't check.
How the Vibely scan fits into this checklist
Every Vibely project has a Security view with two scan depths. A basic scan (code and sensitive data) starts in the background every time you publish. A deep scan adds dependencies, row-level security and database checks, and you run it from the Security view whenever you like. Both are free on every plan. Findings come in three severities (Error, Warning, Info) and roll up into a score out of 100: each open Error costs 25 points, each Warning 5 and each Info 1. A score of 90 or more reads "Excellent", 75 "Good", 60 "Fair", and anything lower "At risk".
Every finding has a Fix button that writes a chat prompt naming the finding, the file, the line and the suggested remediation, so you review the change like any other diff. Try to fix all does the same for every open finding. If a finding is acceptable in your app, you can ignore it with a reason, and the reason is logged with your name. For the walkthrough of the Security view itself, see how to scan your app for security issues.
1. Leaked API keys in source
Why it matters. A key pasted into a file is a key you've published. It ends up in your Git history, in exported zips and in any screenshot of the editor. Agents make this worse because "just put the key here for now" is the shortest path to a working demo. A live Stripe secret key or an AWS access key in source is a direct line to your money or your infrastructure.
How to check. Search the project for vendor prefixes (sk_live_, sk_test_, AKIA, AIza, sb_secret_, ghp_) and for long random strings assigned to names like apiKey, secret or token. Check your .env files too: a key there is in the right place only if the file is never committed or shared.
How to fix. Rotate the key at the provider first, because the old value is already out. Then store the new one as a server-side secret (a Supabase Edge Function secret or a project environment variable) and read it only from server code.
What Vibely checks. The code scanner runs on every basic and deep scan. It matches 211 vendor credential formats from the open-source gitleaks ruleset, plus Vibely's own rules for Stripe, Supabase, AWS, Google and OpenAI-style keys and for JWTs. It doesn't flag every random-looking string. It decodes a Supabase JWT and reads its role claim, so the public anon key is ignored and a service_role key is always critical. It skips placeholders like your-key-here, and it applies an entropy floor (3.5 bits per character) so ordinary identifiers don't trip it. A secret in source is an Error. The same secret in a .env file is a Warning, because that's the right place for it. The exception is a secret bound to a public prefix, which stays an Error (see the next item). Separately, the composer warns you when a chat message you're about to send looks like it contains a secret, so a key you paste into chat gets flagged too. On Business, a workspace can make that warning ask for confirmation or block the message outright.
2. Secrets in the client bundle
Why it matters. Frontend frameworks copy some environment variables into the JavaScript they ship to every visitor. Next.js inlines variables prefixed NEXT_PUBLIC_, Vite inlines VITE_, and Expo inlines EXPO_PUBLIC_. Put a secret behind one of those prefixes and it's readable in the browser's dev tools, even if your .env file is perfectly gitignored. Reading a server-only variable from a client component is the mirror-image mistake. In Next.js it quietly becomes undefined in the browser, and the tempting "fix" is to add the public prefix, which ships the secret.
How to check. List every variable with a public prefix and ask of each one: would I be fine posting this on a forum? Then search client components ('use client' files) for process.env. Open the deployed site, view source and search the JavaScript for the first few characters of each secret.
How to fix. Move the call that needs the secret into a server function, an API route or an Edge Function, and have the client call that. Only values that are public by design, such as a Supabase URL, the anon key or a Stripe publishable key, belong behind a public prefix.
What Vibely checks. On web projects, the code scanner reports an Error when a file marked 'use client' reads a process.env variable that has no public prefix. It also reports any credential bound to a NEXT_PUBLIC_, VITE_, EXPO_PUBLIC_, REACT_APP_ or PUBLIC_ name as an Error, even inside a .env file, because the bundler will ship it. On mobile projects, it warns when a token, password, API key or session is written to AsyncStorage, which is plain text on the device. The fix it suggests is expo-secure-store. Check manually: Vite code that reads import.meta.env is covered by the public-prefix rule but not by the client-component rule.
3. Supabase row-level security
Why it matters. This is the most common serious flaw in a generated app. Supabase exposes your tables through an API that the browser calls directly with the anon key. Row-level security (RLS) policies are the only thing deciding which rows each caller can read or change. If a table has RLS turned off, or a policy says using (true), any visitor can read the whole table with a single request. You won't notice, because while you're the only user, "every row" and "my rows" are the same thing.
How to check. In the Supabase dashboard, confirm RLS is enabled on every table in the public schema, and read each policy. A policy should compare a column to auth.uid() or to a tenant the user belongs to. Then sign in as a second test user and try to load the first user's data.
How to fix. Enable RLS, then write one policy per operation that scopes rows to their owner, for example using ((select auth.uid()) = user_id). Don't base authorization on user_metadata, which users can edit themselves. For a view, set security_invoker = true so it follows the underlying table's policies.
What Vibely checks. The deep scan connects to your linked Supabase project and runs two kinds of checks. First, it pulls Supabase's own database advisor and files these lints under row-level security: RLS disabled on a public table, RLS enabled with no policy, a policy that exists while RLS is off, auth users exposed through a view, policies that reference user metadata, security-definer views, and per-row auth.uid() calls. Second, it queries pg_policies for policies in the public schema whose USING clause is empty or true, and reports each one as an Error. It also flags SECURITY DEFINER functions in public and suggests locking their search_path. If no Supabase project is linked, both database scanners report an Info note and the rest of the scan still runs. When the agent builds your backend, it writes the migrations and RLS policies, but the scan is how you confirm what's actually live in the database.
4. Auth mistakes
Why it matters. Sign-in is easy to get working and easy to get subtly wrong. The usual failures: an admin page that checks the role in the browser but not on the server, an Edge Function that accepts calls with no token, a password policy that accepts passwords already known from breaches, and redirect URLs that let an attacker grab a sign-in code.
How to check. For every route or function that changes data, ask where the user is verified. The answer should be on the server, not in a React component. In Supabase, check your Auth settings: the allowed redirect URLs, email confirmation, and leaked-password protection.
How to fix. Verify the session on the server for every privileged action. Keep admin checks in RLS policies or server code. Restrict redirect URLs to your real domains. Use PKCE for any OAuth flow you build yourself (see OAuth PKCE).
What Vibely checks. The Security view has a Leaked password protection switch that reads and sets Supabase's breached-password check on your linked project, so you can see and change it without opening the Supabase dashboard. The deep scan's advisor checks flag exposed auth tables and policies that trust user metadata. When Vibely deploys your Edge Functions, it keeps JWT verification on unless the project explicitly declares a function public, such as a webhook receiver. Check manually: server-side role checks in your own routes, and your Supabase redirect URL allowlist. No scanner rule reads those.
5. Input validation and injection
Why it matters. Anything a user types, uploads or puts in a link is attacker-controlled. Rendering it as raw HTML lets a script run in other users' browsers (cross-site scripting). Passing it to eval runs arbitrary code. On mobile, a deep link is input too: a handler that navigates or fetches based on any incoming URL can be driven by a crafted link.
How to check. Search for dangerouslySetInnerHTML, .innerHTML =, eval( and new Function(. For each API route and Edge Function, check that the request body is validated against a schema before it's used. Look for SQL built by concatenating strings.
How to fix. Render user content as text. If you truly need HTML, sanitise it with DOMPurify first. Validate request bodies with a schema library such as Zod, and use parameterised queries or the Supabase client instead of building SQL strings. On mobile, parse incoming URLs and check the host and path against an allowlist before you act on them.
What Vibely checks. The code scanner warns on eval, new Function and string-form setTimeout in every project. On web projects it warns on dangerouslySetInnerHTML and direct .innerHTML assignment. On mobile projects it warns on a Linking URL handler when the file shows no sign of validating the URL. Check manually: schema validation of request bodies and SQL injection. The scanner has no rule for either.
6. Dependency CVEs
Why it matters. A generated app pulls in dozens of npm packages, and each one brings in its own dependencies. Known vulnerabilities in those packages are published, indexed and easy to exploit, so an old version is a known way in.
How to check. Run npm audit against the installed tree, or look your package.json up in the OSV vulnerability database. Prioritise packages that run in production over build tools.
How to fix. Upgrade to the patched version. Minor and patch upgrades are usually safe. A major upgrade can change behaviour, so build and test after it.
What Vibely checks. The deep scan runs npm audit on production dependencies inside the project's live sandbox. If no sandbox is running, it checks your package.json against OSV.dev instead and adds an Info note telling you that a full audit needs the project open. Auto-fix rewrites a direct dependency in package.json to the fixed version only when the fix isn't a major-version bump, and marks those findings fixed. It leaves major bumps, and anything outside your own package.json, to you. The Security view also lets you export the dependency findings as JSON.
7. CORS
Why it matters. CORS headers tell browsers which other sites may call your API and read the response. Access-Control-Allow-Origin: * combined with cookie-based sessions, or an API that echoes back whatever origin the request sends, lets any website make authenticated requests on your users' behalf.
How to check. Find every place your server or Edge Functions set Access-Control-Allow-Origin. Then send a request with Origin: https://example.com and see what comes back.
How to fix. Allow only your own domains. Only send Access-Control-Allow-Credentials: true with an explicit origin, never with a wildcard. A public, read-only endpoint can use * if it handles no user data.
What Vibely checks. Check manually. No scanner rule reads CORS headers.
8. Rate limits
Why it matters. Without a limit, one script can try thousands of passwords, flood your contact form, or run up your AI and email bills overnight. Generated apps rarely add limits unless you ask, because nothing in a demo needs one.
How to check. List the endpoints that cost money or protect accounts: sign-in, sign-up, password reset, anything that sends email or SMS, and anything that calls a paid AI model. For each one, ask what stops someone calling it 10,000 times.
How to fix. Review your Supabase project's Auth rate limits. For your own endpoints, add a per-user or per-IP limit, for example a counter table in Postgres or a key-value store in the function. Put spending caps on paid APIs at the provider.
What Vibely checks. Check manually. The scanner doesn't look for rate limits in your app's code.
9. Storage buckets
Why it matters. A public Supabase Storage bucket serves any file to anyone who has, or can guess, its URL. That's fine for product images and dangerous for invoices, ID scans or user uploads. Private buckets also depend on policies on storage.objects, and a loose policy there is the RLS mistake all over again.
How to check. In Supabase, list your buckets and note which are public. For each private bucket, read the policies on storage.objects and confirm users can only reach their own folder.
How to fix. Keep user uploads in private buckets, scope storage policies to the owner (for example by a folder named after auth.uid()), and serve files through short-lived signed URLs.
What Vibely checks. Buckets the agent creates are private unless you ask for a public one. A workspace setting, Block public storage buckets (Settings → Privacy & security), forces every agent-created bucket private even when one is requested. Check manually: buckets made in the Supabase dashboard, and the policies on storage.objects. The scanner's permissive-policy probe covers the public schema only.
10. Logging and sensitive data
Why it matters. Logs are copied, retained and read by more people than your database. A console.log(user) left in an Edge Function writes emails, tokens and addresses to a log viewer that anyone on the project can open. Real personal data used as test fixtures leaks the same way, through the repo and every version of it.
How to check. Search server code and Edge Functions for console.log calls that print whole request bodies, user objects, headers or tokens. Check seed files and fixtures for real names, emails and card numbers.
How to fix. Log IDs and event names, not payloads. Redact tokens and personal fields before anything is written. Replace real records in fixtures with obvious fakes.
What Vibely checks. The sensitive-data scanner runs on every basic and deep scan and reads your source files for hardcoded personal data. It reports card numbers that pass a Luhn checksum, US Social Security numbers and IBANs as Warnings, and email addresses and phone numbers as Info, with one finding per file per type and the value redacted in the excerpt. A workspace can switch this scanner off only on Business. The Security center on Business lists your project secrets by name and location only, never by value. Check manually: what your code writes to logs at runtime. The scanner reads files, not logs.
11. Publishing settings
Why it matters. Every check above is advice until something stops a bad version from going live. Publishing is the last gate, and it's the right place for rules a team agrees on once.
How to check. Before launch, confirm who can publish, whether a scan has run, and whether any Error findings are still open. In the code, look for plain http:// URLs and, on mobile, for settings that allow cleartext traffic.
How to fix. Turn on the publishing guardrails below, run a deep scan, clear every Error, and scan again after the fixes.
What Vibely checks. Settings → Privacy & security has these controls, and web publishes and mobile store submissions share the same gate:
- Block publishing with critical issues: refuses a publish while the project has any open Error finding. Every plan.
- Require basic security scan before first publish: refuses a project's first publish until a scan has completed. Every plan.
- Block publishing with PII: refuses a publish while sensitive-data findings are open. Business.
- Who can publish externally: limits publishing to editors and above, or to the owner. Business.
- Require two-factor authentication to publish. Business.
Note how these fit together. The basic scan that starts on each publish doesn't hold that publish back. It runs in the background, and its findings feed the next publish's gate. So run a deep scan yourself before your first real launch. The code scanner also reports plain http:// URLs as Info (localhost excluded), and on mobile it warns on usesCleartextTraffic, NSAllowsArbitraryLoads and a WebView with mixedContentMode="always". On Business, the Security center can schedule weekly or monthly deep scans across a workspace's projects.
The ten-minute version before you launch
- Run a deep scan from the Security view and clear every Error.
- Rotate any key that has ever appeared in source, chat, a screenshot or a shared log.
- Sign in as a second test user and try to read the first user's data.
- Turn on "Block publishing with critical issues" and "Require basic security scan before first publish".
- Check CORS and rate limits by hand. They're the two items no scanner covers.
- Scan again after the fixes. Findings describe your code as it was when you scanned, not as it is now.
Scanning is free on every plan, and the plan-gated controls above are listed on pricing. For how Vibely protects the platform itself, as opposed to the apps you build, see Vibely security and compliance. New to the term? Start with what vibe coding is.
Frequently asked questions
Is a vibe-coded app less secure than a hand-written one?
Not by nature, but it fails in predictable places. An AI agent writes code that works for the person testing it, and most security bugs are invisible to that person: a policy that lets any signed-in user read every row looks exactly like a working app when you are the only user. The fix is a checklist and a scanner, run before strangers arrive.
Is the Supabase anon key a secret?
No. Supabase documents the anon (and publishable) key as safe to ship in a browser, because row-level security, not secrecy, protects the data behind it. Vibely’s scanner decodes Supabase JWTs and ignores the anon role for that reason. The service_role key and sb_secret_ keys are the opposite: they bypass row-level security and must never reach a client.
Does Vibely’s security scan cost credits?
No. Both the basic scan and the deep scan are free on every plan. A basic scan starts in the background on every publish, and you can run a deep scan from the project’s Security view at any time. Fixing a finding goes through the chat, where the change is billed like any other build turn.
Does a clean scan mean my app is secure?
No. It means none of the patterns the scanners look for were found in the code and database at the time of the scan. Vibely says so in the empty state: the scan cannot catch every possible risk. Authorization logic in your own routes, CORS, rate limits and what you write to logs still need a person to check them.
Can Vibely stop me from publishing an app with critical issues?
Yes, if you turn it on. In Settings → Privacy & security, “Block publishing with critical issues” refuses a publish while the project has open Error findings, and “Require basic security scan before first publish” refuses a first publish with no completed scan. Both are available on every plan. Blocking publishes that contain personal data is a Business setting.
What should I do if a secret was already committed or published?
Rotate it at the provider first: Stripe, Supabase, OpenAI, AWS or whoever issued it. Deleting the line does not help, because the old value is still in your Git history, in any deployed bundle and in any copy someone already took. Then store the new value as a server-side secret and re-run the scan to confirm the finding is gone.
Table of Contents
- 01How the Vibely scan fits into this checklist1 min read
- 021. Leaked API keys in source
- 032. Secrets in the client bundle
- 043. Supabase row-level security
- 054. Auth mistakes
- 065. Input validation and injection
- 076. Dependency CVEs
- 087. CORS
- 098. Rate limits
- 109. Storage buckets
- 1110. Logging and sensitive data
- 1211. Publishing settings
- 13The ten-minute version before you launch
- 14Frequently asked questions